npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-form-action

v1.0.1

Published

State management helpers for the react form actions.

Downloads

148

Readme

react-form-action

End-to-end typesafe success, error & validation state control for Next.js 14 form actions.

Features

Action Creator

  • ✅ Provides "invalid" | "success" | "failure" response objects.
  • ✅ Define generic payload for each of the response type.

Form Action builder

  • ✅ tRPC-like builder API for .input(zodSchema) & context .use(middleware)
  • ✅ Parses formData with zod-form-data

Stateful <Form />

  • <Form /> component reads the action's response.
  • ✅ Computes progress meta-state like isInvalid, isSuccess and more.

Install

npm i react-form-action zod-form-data

Usage

formAction builder

// app/actions/auth.ts
"use server";

import { formAction } from "react-form-action";
import { z } from "zod";
import { cookies } from "next/headers";

const i18nMiddleware = async () => {
  const { t } = await useTranslation("auth", cookies().get("i18n")?.value);
  // will be added to context
  return { t };
};

const authAction = formAction
  .use(i18nMiddleware)
  .use(async ({ ctx: { t } }) =>
    console.log("🎉 context enhanced by previous middlewares 🎉", t)
  )
  .error(({ error }) => {
    if (error instanceof DbError) {
      return error.custom.code;
    } else {
      // unknown error
      // default Next.js error handling (error.js boundary)
      throw error;
    }
  });

export const signIn = authAction
  .input(z.object({ email: z.string().email() }))
  // 🎉 extend the previous input (only without refinements and transforms)
  .input(z.object({ password: z.string() }))
  .run(async ({ ctx: { t }, input: { email, password } }) => {
    // Type inferred: {email: string, password: string}

    await db.signIn({ email, password });

    return t("verificationEmail.success");
  });

export const signUp = authAction
  .input(
    z
      .object({
        email: z.string().email(),
        password: z.string(),
        confirm: z.string(),
      })
      .refine((data) => data.password === data.confirm, {
        message: "Passwords don't match",
        path: ["confirm"],
      })
  ) // if using refinement, only one input call is permited, as schema with ZodEffects is not extendable.
  .run(async ({ ctx: { t }, input: { email, password } }) => {
    // 🎉 passwords match!

    const tokenData = await db.signUp({ email, password });

    if (!tokenData) {
      return t("signUp.emailVerificationRequired");
    }

    return t("singUp.success");
  });

Server Action (usable in Next.js)

"use server";

import { createFormAction } from "react-form-action";
import { z } from "zod";

// Define custom serializable error & success data types
type ErrorData = {
  message: string;
};

type SuccessData = {
  message: string;
};

type ValiationError = {
  name?: string;
};

const updateUserSchema = z.object({ name: z.string() });

export const updateUser = createFormAction<
  SuccessData,
  ErrorData,
  ValiationError
>(({ success, failure, invalid }) =>
  // success and failure helper functions create wrappers for success & error data respectively
  async (prevState, formData) => {
    if (prevState.type === "initial") {
      // use the initialData passed to <Form /> here
      // prevState.data === "foobar"
    }

    try {
      const { name } = updateUserSchema.parse({
        name: formData.get("name"),
      });

      const user = await updateCurrentUser(name);

      if (user) {
        // {type: "success", data: "Your profile has been updated.", error: null, validationError: null}
        return success({
          message: "Your profile has been updated.",
        });
      } else {
        // {type: "error", data: null, error: { message: "No current user." }, validationError: null}
        return failure({ message: "No current user." });
      }
    } catch (error) {
      if (error instanceof ZodError) {
        // {type: "invalid", data: null, error: null, validationError: {name: "Invalid input"}}
        return invalid({
          name: error.issues[0]?.message ?? "Validation error",
        });
      }

      return failure({ message: "Failed to update user." });
    }
  }
);

<Form> Component

"use client";
// Form connects the formAction to the formState & provides the meta state props via render prop
import { Form } from "react-form-action/client";

import { updateUser } from "@/actions";

export function UpdateUserForm() {
  return (
    <Form action={updateUser} initialData="foobar">
      {({
        error,
        data,
        validationError,
        isPending,
        isFailure,
        isInvalid,
        isSuccess,
        isInitial,
      }) => (
        <>
          {/* safely access the data or error by checking the mutually exclusive boolean flags: */}
          {isSuccess && <p className="success-message">{data}</p>}
          {isFailure && <p className="error-message">{error.message}</p>}
          <input type="text" name="name" disabled={isPending} />
          {isInvalid && (
            <span className="input-error">{validationError.name}</span>
          )}

          <button disabled={isPending}>
            {isPending ? "Submitting..." : "Submit"}
          </button>
        </>
      )}
    </Form>
  );
}

Bonus <FormStatus>

The useFormStatus hook data exposed via render prop:

import { FormStatus } from "react-form-action/client";

// <FormStatus> alleviates the need to create a separate <SubmitButton> component using the useFormStatus hook

return function MyForm() {
  return (
    <form action={action}>
      <FormStatus>
        {({ pending }) => (
          <button type="submit">{pending ? "Submitting..." : "Submit"} </button>
        )}
      </FormStatus>
    </form>
  );
};