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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yedoma-labs/suruy-form-actions

v0.1.0

Published

Type-safe form library for React Server Actions with progressive enhancement

Readme

@yedoma-labs/suruy-form-actions

suruy (Yakut: суруй) — verb. write, inscribe

Type-safe form library for React Server Actions with progressive enhancement

Features

  • Zero runtime dependencies - Lightweight and fast
  • 🎯 Type-safe - Full TypeScript support with inference
  • 🚀 React 19 ready - Uses useActionState for optimal UX
  • 📝 Progressive enhancement - Works without JavaScript
  • 🎨 Flexible validation - Built-in validator or bring your own (Zod, Valibot, etc.)
  • 🔒 Server-first - Validation runs on the server
  • 🪶 Tiny bundle - ~3KB gzipped

Installation

pnpm add @yedoma-labs/suruy-form-actions

Quick Start

1. Create a form action

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

import { createFormAction, schema } from "@yedoma-labs/suruy-form-actions";

const loginSchema = schema<{ email: string; password: string }>({
  email: { type: "email", required: true },
  password: { type: "string", required: true, min: 8 },
});

export const loginAction = createFormAction(
  (formData) => loginSchema.safeParse(Object.fromEntries(formData)),
  async (data) => {
    // Your business logic here
    const user = await db.user.findUnique({ where: { email: data.email } });
    
    if (!user) {
      return { success: false, errors: { email: ["User not found"] } };
    }

    return { success: true, data: { userId: user.id } };
  }
);

2. Use in a component

// app/login/page.tsx
"use client";

import { useFormAction } from "@yedoma-labs/suruy-form-actions";
import { loginAction } from "./actions";

export default function LoginPage() {
  const { state, action, pending, formRef } = useFormAction(loginAction, {
    onSuccess: (data) => {
      console.log("Logged in as", data.userId);
    },
    resetOnSuccess: true,
  });

  return (
    <form ref={formRef} action={action}>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" required />
        {state.errors?.email && (
          <span role="alert">{state.errors.email[0]}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input id="password" name="password" type="password" required />
        {state.errors?.password && (
          <span role="alert">{state.errors.password[0]}</span>
        )}
      </div>

      <button type="submit" disabled={pending}>
        {pending ? "Logging in..." : "Log in"}
      </button>

      {state.errors?._form && (
        <div role="alert">{state.errors._form[0]}</div>
      )}
    </form>
  );
}

API Reference

createFormAction(validator, handler)

Create a type-safe form action with validation.

const action = createFormAction<Input, Output>(
  validator,  // (formData: FormData) => Promise<ValidationResult>
  handler     // (data: Input) => Promise<ActionResult<Output>>
);

Returns: A form action compatible with React's useActionState

createSimpleAction(handler)

Create a form action without validation (for simple use cases).

const action = createSimpleAction(async (formData) => {
  const name = formData.get("name");
  return { success: true, data: { message: `Hello ${name}` } };
});

useFormAction(action, options?)

React hook to manage form state on the client.

const { state, action, pending, formRef } = useFormAction(myAction, {
  onSuccess: (data) => void,
  onError: (errors) => void,
  resetOnSuccess: true,
});

Returns:

  • state - Current form state (data, errors, pending, success)
  • action - Function to pass to <form action={...}>
  • pending - Boolean indicating if submission is in progress
  • formRef - Ref to attach to form element (for auto-reset)

schema(fields)

Built-in zero-dependency validator (alternative to Zod).

const userSchema = schema<{ name: string; age: number }>({
  name: {
    type: "string",
    required: true,
    min: 2,
    max: 50,
  },
  age: {
    type: "number",
    required: true,
    min: 18,
    max: 120,
  },
  email: {
    type: "email",
    required: true,
  },
  website: {
    type: "url",
    required: false,
  },
});

Supported types: string, number, boolean, email, url

Constraints: required, min, max, pattern (regex), custom (function)

parseFormData(formData)

Parse FormData into a plain object.

const data = parseFormData(formData);
// { name: "John", tags: ["a", "b"] }

Handles array fields with [] suffix automatically.

Using with Zod

import { createFormAction } from "@yedoma-labs/suruy-form-actions";
import { z } from "zod";

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

const validator = async (formData: FormData) => {
  const result = loginSchema.safeParse(Object.fromEntries(formData));
  
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    };
  }
  
  return { success: true, data: result.data };
};

export const loginAction = createFormAction(validator, async (data) => {
  // ...
});

Progressive Enhancement

Forms work without JavaScript by default:

// This form submits to the server even if JS is disabled
<form action={myAction}>
  <input name="email" type="email" required />
  <button>Submit</button>
</form>

The server will:

  1. Validate the input
  2. Process the action
  3. Return errors or redirect

With JavaScript enabled, you get:

  • Loading states (pending)
  • Client-side error display
  • No page refresh
  • onSuccess/onError callbacks

Examples

Multi-field validation

const registerSchema = schema<{
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}>({
  username: {
    type: "string",
    required: true,
    min: 3,
    custom: (value) => {
      if (!/^[a-z0-9_]+$/.test(value)) {
        return "Only lowercase letters, numbers, and underscores";
      }
      return null;
    },
  },
  email: { type: "email", required: true },
  password: { type: "string", required: true, min: 8 },
  confirmPassword: { type: "string", required: true },
});

File uploads

const uploadAction = createSimpleAction(async (formData) => {
  const file = formData.get("avatar") as File;
  
  if (file.size > 5_000_000) {
    return { success: false, errors: { avatar: ["File too large"] } };
  }
  
  const url = await uploadToS3(file);
  return { success: true, data: { url } };
});

Optimistic updates

const { state, action, pending } = useFormAction(addTodoAction, {
  onSuccess: (data) => {
    // Optimistically add to UI
    setTodos(prev => [...prev, data.todo]);
  },
});

Why suruy-form-actions?

| Feature | suruy-form-actions | React Hook Form | Formik | Conform | |---------|-----------|----------------|--------|---------| | Bundle size | ~3KB | 12KB | 44KB | 8KB | | Server Actions | ✅ | ⚠️ Manual | ❌ | ✅ | | Zero deps | ✅ | ✅ | ❌ | ✅ | | Progressive enhancement | ✅ | ❌ | ❌ | ✅ | | Built-in validator | ✅ | ❌ | ❌ | ❌ | | TypeScript | ✅ | ✅ | ✅ | ✅ |

Project Structure

suruy-form-actions/
├── src/
│   ├── index.ts          # Public API
│   ├── types.ts          # TypeScript types
│   ├── action.ts         # Form action creators
│   ├── hooks.ts          # React hooks
│   ├── validation.ts     # Built-in validator
│   └── *.test.ts         # Unit tests
├── dist/                 # Build output
├── .github/workflows/
│   ├── ci.yml            # Lint, test, build on push+PR
│   └── release.yml       # Publish to npm on git tag
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Build
pnpm build

# Lint
pnpm lint

License

MIT © yedoma-labs