{"id":18162869,"url":"https://github.com/miroslavpetrik/react-form-action","last_synced_at":"2025-06-10T13:05:24.757Z","repository":{"id":206851330,"uuid":"717838091","full_name":"MiroslavPetrik/react-form-action","owner":"MiroslavPetrik","description":"tRPC like builder for the Next.js \u0026 React form actions.","archived":false,"fork":false,"pushed_at":"2025-02-03T17:36:17.000Z","size":265,"stargazers_count":23,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-07T18:04:03.923Z","etag":null,"topics":["next-form-action","nextjs","react","react-form-action","server-action","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MiroslavPetrik.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-11-12T18:54:46.000Z","updated_at":"2025-06-03T12:18:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"83f41707-c76a-4d54-9133-9a9f0f3f1837","html_url":"https://github.com/MiroslavPetrik/react-form-action","commit_stats":{"total_commits":25,"total_committers":1,"mean_commits":25.0,"dds":0.0,"last_synced_commit":"45d9f79629517a0e6e48eab02fddb157e5510afb"},"previous_names":["miroslavpetrik/react-form-action"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MiroslavPetrik%2Freact-form-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MiroslavPetrik%2Freact-form-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MiroslavPetrik%2Freact-form-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MiroslavPetrik%2Freact-form-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MiroslavPetrik","download_url":"https://codeload.github.com/MiroslavPetrik/react-form-action/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MiroslavPetrik%2Freact-form-action/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259081014,"owners_count":22802400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["next-form-action","nextjs","react","react-form-action","server-action","typescript"],"created_at":"2024-11-02T10:05:27.748Z","updated_at":"2025-06-10T13:05:24.730Z","avatar_url":"https://github.com/MiroslavPetrik.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-form-action\n\nEnd-to-end typesafe success, error \u0026 validation state control for Next.js form actions.\n\n## Features\n\n**Action Creator**\n\n- ✅ Provides envelope objects with `\"initial\" | \"invalid\" | \"success\" | \"failure\"` response types.\n- ✅ Define generic payload for each of the response type.\n- ✅ Bind action arguments.\n\n**tRPC-like Form Action builder**\n\n- ✅ Define payload schema with the `.input(zodSchema)` to validate the `formData`\n- ✅ Reuse business logic with the `.use(middleware)` method.\n- ✅ Define bindable arguments with the `.args([])` method.\n- ✅ Reuse error handling with the `.error(handler)`.\n\n**React Context access with the `\u003cAction action={myFormAction} /\u003e` component**\n\n- ✅ The `useActionState()` accessible via the `useActionContext()` hook.\n- ✅ Computes progress flags like `isInvalid`, `isSuccess` based on the envelope type.\n\n**Context-bound `\u003cForm /\u003e` component**\n\n- ✅ Reads the `action` from the `\u003cAction /\u003e` context.\n- ✅ Opt-out from the default form reset after action submit.\n\n## Install\n\n```\nnpm i react-form-action zod-form-data\n```\n\n\u003ca aria-label=\"NPM version\" href=\"https://www.npmjs.com/package/react-form-action\"\u003e\n  \u003cimg alt=\"NPM Version\" src=\"https://img.shields.io/npm/v/react-form-action?style=for-the-badge\u0026labelColor=24292e\"\u003e\n\u003c/a\u003e\n\n## Getting Started\n\n#### 1️⃣ Create a Server Action\n\n```tsx\n// app/subscribe/action.ts\n\"use server\";\n\nimport { formAction } from \"react-form-action\";\nimport { z } from \"zod\";\n\nexport const subscribeAction = formAction\n  .input(z.object({ email: z.string().email() }))\n  .run(async ({ input }) =\u003e {\n    return input.email;\n  });\n```\n\n#### 2️⃣ Create a Client Form Component\n\n```tsx\n// app/subscribe/form.tsx\n\"use client\";\n\nimport {\n  Form,\n  Pending,\n  createComponents,\n  useActionContext,\n} from \"react-form-action/client\";\n\nimport { subscribeAction } from \"./action\";\n\nconst { FieldError, Success } = createComponents(subscribeAction);\n\nexport function SubscribeForm() {\n  const { isPending, isFailure, error, data } =\n    useActionContext(subscribeAction);\n\n  return (\n    \u003cForm\u003e\n      \u003cSuccess\u003e\n        \u003cp\u003e✅ Email {data} was registered.\u003c/p\u003e\n      \u003c/Success\u003e\n      {/*💡 The FieldError \"name\" prop supports autocompletion */}\n      \u003cFieldError name=\"email\"\u003e\n        {({ name, error }) =\u003e (\n          \u003cinput className={error ? \"invalid\" : \"\"} name={name} /\u003e\n        )}\n      \u003c/FieldError\u003e\n      \u003cbutton type=\"submit\" disabled={isPending}\u003e\n        {isPending ? \"🌀 Submitting...\" : \"Submit\"}\n      \u003c/button\u003e\n      \u003cPending\u003ePlease wait...\u003c/Pending\u003e\n    \u003c/Form\u003e\n  );\n}\n```\n\n#### 3️⃣ Provide the `\u003cAction\u003e` context on a Page\n\n```tsx\n// app/subscribe/page.tsx\n\nimport { Action } from \"react-form-action/client\";\n\nimport { subscribeAction } from \"./action\";\nimport { SubscribeForm } from \"./form\";\n\nexport default function Page() {\n  return (\n    \u003cAction action={subscribeAction} initialData=\"\"\u003e\n      \u003cSubscribeForm /\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n## Usage\n\n### `formAction` builder\n\nThe [`zod-form-data`](https://www.npmjs.com/package/zod-form-data) powered action builder.\n\n```ts\n// app/actions/auth.ts\n\"use server\";\n\nimport { formAction } from \"react-form-action\";\nimport { z } from \"zod\";\nimport { cookies } from \"next/headers\";\n\nconst i18nMiddleware = async () =\u003e {\n  const { t } = await useTranslation(\"auth\", cookies().get(\"i18n\")?.value);\n  // will be added to context\n  return { t };\n};\n\nconst authAction = formAction\n  .use(i18nMiddleware)\n  .use(async ({ ctx: { t } }) =\u003e\n    console.log(\"🎉 context enhanced by previous middlewares 🎉\", t)\n  )\n  .error(async ({ error }) =\u003e {\n    if (error instanceof DbError) {\n      return error.custom.code;\n    } else {\n      // unknown error\n      // default Next.js error handling (error.js boundary)\n      throw error;\n    }\n  });\n\nexport const signIn = authAction\n  .input(z.object({ email: z.string().email() }))\n  // 🎉 extend the previous input (only without refinements and transforms)\n  .input(z.object({ password: z.string() }))\n  .run(async ({ ctx: { t }, input: { email, password } }) =\u003e {\n    // Type inferred: {email: string, password: string}\n\n    await db.signIn({ email, password });\n\n    return t(\"verificationEmail.success\");\n  });\n\nexport const signUp = authAction\n  .input(\n    z\n      .object({\n        email: z.string().email(),\n        password: z.string(),\n        confirm: z.string(),\n      })\n      .refine((data) =\u003e data.password === data.confirm, {\n        message: \"Passwords don't match\",\n        path: [\"confirm\"],\n      })\n  ) // if using refinement, only one input call is permited, as schema with ZodEffects is not extendable.\n  .run(async ({ ctx: { t }, input: { email, password } }) =\u003e {\n    // 🎉 passwords match!\n\n    const tokenData = await db.signUp({ email, password });\n\n    if (!tokenData) {\n      return t(\"signUp.emailVerificationRequired\");\n    }\n\n    return t(\"singUp.success\");\n  });\n```\n\n#### Args binding\n\nThe `formAction` builder supports action arguments binding:\n\n```ts\n// app/update-user/[userId]/action.tsx\nimport { formAction } from \"react-form-action\";\n\nexport const updateUser = formAction\n  .args([z.string().uuid()])\n  .run(async ({ args: [userId] }) =\u003e {\n    return userId;\n    //     ^? string\n  });\n```\n\n```tsx\n// app/update-user/[userId]/page.tsx\nimport { Action } from \"react-form-action/client\";\n\nimport { updateUser } from \"./action\";\nimport { UpdateUserForm } from \"./form\";\n\nexport default function Page({\n  params,\n}: {\n  params: Promise\u003c{ userId: string }\u003e;\n}) {\n  const { userId } = await params;\n\n  const action = updateUser.bind(null, userId);\n\n  return (\n    \u003cAction action={action} initialData=\"\"\u003e\n      \u003cSubscribeForm /\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n### Action Creator\n\nLow-level action creator, which provides the `success`, `failure` and `invalid` envelope constructors. With the `createFormAction` you must handle the native `FormData` by yourself.\n\n```ts\n\"use server\";\n\nimport { createFormAction } from \"react-form-action\";\nimport { z } from \"zod\";\n\n// Define custom serializable error \u0026 success data types\ntype ErrorData = {\n  message: string;\n};\n\ntype SuccessData = {\n  message: string;\n};\n\ntype ValiationError = {\n  name?: string;\n};\n\nconst updateUserSchema = z.object({ name: z.string() });\n\nexport const updateUser = createFormAction\u003c\n  SuccessData,\n  ErrorData,\n  ValiationError\n\u003e(({ success, failure, invalid }) =\u003e\n  // success and failure helper functions create wrappers for success \u0026 error data respectively\n  async (prevState, formData) =\u003e {\n    if (prevState.type === \"initial\") {\n      // use the initialData passed to \u003cForm /\u003e here\n      // prevState.data === \"foobar\"\n    }\n\n    try {\n      const { name } = updateUserSchema.parse({\n        name: formData.get(\"name\"),\n      });\n\n      const user = await updateCurrentUser(name);\n\n      if (user) {\n        // {type: \"success\", data: \"Your profile has been updated.\", error: null, validationError: null}\n        return success({\n          message: \"Your profile has been updated.\",\n        });\n      } else {\n        // {type: \"error\", data: null, error: { message: \"No current user.\" }, validationError: null}\n        return failure({ message: \"No current user.\" });\n      }\n    } catch (error) {\n      if (error instanceof ZodError) {\n        // {type: \"invalid\", data: null, error: null, validationError: {name: \"Invalid input\"}}\n        return invalid({\n          name: error.issues[0]?.message ?? \"Validation error\",\n        });\n      }\n\n      return failure({ message: \"Failed to update user.\" });\n    }\n  }\n);\n```\n\nThe action creator supports arguments binding:\n\n```ts\nexport const updateUser = createFormAction(\n  (\n    { success, failure, invalid },\n    userId: string /* Here you can specify multiple arguments */\n  ) =\u003e\n    async (prevState, formData) =\u003e {\n      try {\n        const { name } = updateUserSchema.parse({\n          name: formData.get(\"name\"),\n        });\n\n        const user = await db.users.findById(userId);\n\n        if (!user) {\n          return failure({ message: \"No such user.\" });\n        }\n\n        const updated = await user.update({ name });\n\n        if (updated) {\n          return success({\n            message: \"User has been updated.\",\n          });\n        } else {\n          return failure({ message: \"Failed to update.\" });\n        }\n      } catch (error) {\n        // handle error\n      }\n    }\n);\n\n// call bind as usuall, the \"123\" becomes the \"userId\"\nupdateUser.bind(null, \"123\");\n```\n\n### Action Context\n\nThe `\u003cAction\u003e` components enables you to access your `action`'s state with the `useActionContext()` hook:\n\n```tsx\n// 👉 Define standalone client form component (e.g. /app/auth/signup/SignUpForm.tsx)\n\"use client\";\n\nimport { Action, Form, useActionContext } from \"react-form-action/client\";\nimport type { PropsWithChildren } from \"react\";\n\nimport { signupAction } from \"./action\";\n\nfunction Pending({ children }: PropsWithChildren) {\n  // read any state from the ActionContext:\n  const {\n    error,\n    data,\n    validationError,\n    isPending,\n    isFailure,\n    isInvalid,\n    isSuccess,\n    isInitial,\n  } = useActionContext();\n\n  return isPending \u0026\u0026 children;\n}\n\n// 💡 render this form on your RSC page (/app/auth/signup/page.tsx)\nexport function SignupForm() {\n  return (\n    \u003cAction action={signupAction}\u003e\n      \u003cForm\u003e\n        \u003cinput name=\"email\" /\u003e\n        \u003cinput name=\"password\" /\u003e\n      \u003c/Form\u003e\n      {/* 🎆 Read the pending state outside the \u003cForm\u003e */}\n      \u003cPending\u003e\n        {/* This renders only when the action is pending. 😎 */}\n        \u003cp\u003ePlease wait...\u003c/p\u003e\n      \u003c/Pending\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n### `\u003cForm\u003e` Component\n\nThe `\u003cform\u003e` submits the action in `onSubmit` handler to [prevent automatic form reset](https://github.com/facebook/react/issues/29034).\nPass `autoReset` prop to use the `action` prop instead and keep the default reset.\n\n```tsx\n\"use client\";\n\nimport { Action, Form } from \"react-form-action/client\";\n\nimport { updateUser } from \"./action\";\n\nexport function UpdateUserForm() {\n  return (\n    \u003cAction action={updateUser}\u003e\n      \u003cForm autoReset\u003e{/* ... */}\u003c/Form\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n### Context Bound Components `createComponents()`\n\nUse the `createComponents(action)` helper to create components which use the ActionContext and have types bound to the action type.\n\n#### `\u003cFielError\u003e` Component\n\n```tsx\n\"use client\";\n\n// ⚠️ createComponents is usable only in \"use client\" components\nimport { Form, createComponents } from \"react-form-action/client\";\n\nimport { authAction } from \"./actions\";\n\nexport const signUpAction = authAction\n  .input(\n    z\n      .object({\n        user: z.object({\n          email: z.string().email(),\n          name: z.string(),\n        }),\n        password: z.string().min(8),\n        confirm: z.string(),\n      })\n      .refine((data) =\u003e data.password === data.confirm, {\n        message: \"Passwords don't match\",\n      })\n  )\n  .run(async ({ ctx, input }) =\u003e {\n    return null;\n  });\n\n// 🌟 The FieldError is now bound do the signUpAction input schema which allows autocompletion for its \"name\" prop\n// ⚠️ Usable only with actions created with the formAction builder\nconst { FieldError } = createComponents(signUpAction);\n\nexport function SignUpForm() {\n  return (\n    \u003cAction action={signUpAction} initialData={null}\u003e\n      \u003cForm\u003e\n        {/* 1️⃣ When the \"name\" prop is an empty string, the top-level error will be rendered e.g.:\n          \"Passwords don't match\" */}\n        \u003cFieldError name=\"\" /\u003e\n        {/* 2️⃣ Access fields by their name: */}\n        \u003cFieldError name=\"password\" /\u003e\n        \u003cFieldError name=\"confirm\" /\u003e\n        {/* 3️⃣ Access nested fields by dot access notation: */}\n        \u003cFieldError name=\"user.email\" /\u003e\n      \u003c/Form\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n#### `\u003cSuccess\u003e`\n\n#### When children are JSX\n\n```tsx\nimport { Action, createComponents } from \"react-form-action/client\";\n\nconst { Success } = createComponents(signUpAction);\n\nfunction MyForm() {\n  return (\n    \u003cAction action={signUpAction}\u003e\n      \u003cSuccess\u003e\n        {/* 👉 The message will render only after the action has succeeded */}\n        \u003cp\u003eYou've been signed up!\u003c/p\u003e\n      \u003c/Success\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n#### When children is a render prop\n\n```tsx\nimport { createComponents } from \"react-form-action/client\";\n\nconst { Success } = createComponents(signUpAction);\n\nfunction Label({children}: PropsWithChildren) {\n  return (\n    \u003cSuccess\u003e\n      {({ isSuccess, data }) =\u003e (\n        {/* 👉 With a render prop, the children are always mounted, regardles of the isSuccess flag */}\n        \u003clabel className={isSuccess ? \"green\" : \"\"}\u003e\n          {children}\n        \u003c/label\u003e\n      )}\n    \u003c/Success\u003e\n  );\n};\n```\n\n### `\u003cPending\u003e`\n\nRender children when the action is pending:\n\n#### When children are JSX\n\n```tsx\nimport { Action, Pending } from \"react-form-action/client\";\n\nimport { Spinner } from \"./components\";\n\nfunction MyForm() {\n  return (\n    \u003cAction action={action}\u003e\n      {/* 👉 Unlike the React.useFormStatus() hook, we don't need here the \u003cform\u003e element at all. */}\n      \u003cPending\u003e\n        {/* 👉 The spinner will UNMOUNT when the action is NOT pending */}\n        \u003cSpinner /\u003e\n      \u003c/Pending\u003e\n    \u003c/Action\u003e\n  );\n}\n```\n\n#### When children is a render prop\n\n```tsx\nimport { Pending } from \"react-form-action/client\";\n\nfunction SubmitButton() {\n  return (\n    \u003cPending\u003e\n      {({ isPending }) =\u003e (\n        {/* 👉 With a render prop, the children are always mounted, regardles of the isPending flag */}\n        \u003cbutton type=\"submit\" disabled={isPending}\u003e\n          {isPending ? \"Submitting...\" : \"Submit\"}\n        \u003c/button\u003e\n      )}\n    \u003c/Pending\u003e\n  );\n};\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiroslavpetrik%2Freact-form-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmiroslavpetrik%2Freact-form-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiroslavpetrik%2Freact-form-action/lists"}