react-form-actions
v0.0.0
Published
Tree-shakeable React hooks for forms built on Server Actions and Server Components.
Downloads
141
Maintainers
Readme
react-form-actions
Small, tree-shakeable React hooks for forms built on Server Actions and
Server Components. They fill the gaps you hit when a form is driven by
useActionState: keeping typed values after submit, gating the submit button,
and showing a toast from the action result.
- Tree-shakeable. Import the whole set or one hook by its own subpath. You only ship what you use.
- ESM, typed. Ships ESM and
.d.ts.reactis a peer dependency. - Toast-library agnostic. The toast hook works with any library through a
tiny adapter. A ready
sonnerbuild is included.
Install
pnpm add react-form-actions
# or
npm i react-form-actionsreact 18+ is a peer dependency. The useActionState patterns shown below
need React 19.
Hooks
usePreventFormReset
React resets an uncontrolled form once a Server Action returns, which wipes the user's input on every submit. This hook cancels that reset.
"use client"
import { useRef } from "react"
import { usePreventFormReset } from "react-form-actions/use-prevent-form-reset"
export function EditForm({ action }) {
const formRef = useRef<HTMLFormElement>(null)
usePreventFormReset({ formRef })
return (
<form ref={formRef} action={action}>
{/* fields keep their values after submit */}
</form>
)
}Pass either formRef (a ref to the form) or formId (the form's id). Set
forceDisabled to turn the hook off.
useButtonFormStatusDisabling
Keeps the submit button disabled until the form is both changed and valid. It also disables the button while the action is pending. It works on the live DOM, so it needs no controlled inputs.
"use client"
import { useButtonFormStatusDisabling } from "react-form-actions/use-button-form-status-disabling"
export function ProfileForm({ action, isPending, lastSubmittedFormValues }) {
useButtonFormStatusDisabling({
formId: "profile-form",
isPending,
lastSubmittedFormValues,
})
return (
<form id="profile-form" action={action}>
<input name="name" required />
<button type="submit">Save</button>
</form>
)
}Pass lastSubmittedFormValues (the FormData you just submitted) so the hook
re-bases its "changed" check after each successful submit.
useFormShowToast — toast from the action result
This hook reads your action state and fires a success or error toast when the
status changes. It is not tied to any toast library. You bind it once with
createFormToast.
Any toast library
// lib/form-toast.ts
import { createFormToast } from "react-form-actions/use-form-show-toast"
import { myToast } from "./my-toast"
// myToast must expose: success(message, { duration }) and error(message, { duration })
export const useFormShowToast = createFormToast(myToast)"use client"
import { useActionState } from "react"
import { useFormShowToast } from "@/lib/form-toast"
export function DeleteForm({ deleteAction }) {
const [state, action] = useActionState(deleteAction, { message: "", status: 0 })
useFormShowToast({
state,
withSuccessToast: true,
withErrorToast: true,
onSuccess: () => {
/* close a dialog, redirect, etc. */
},
})
return <form action={action}>{/* ... */}</form>
}Using sonner — zero setup
If you use sonner, import the ready hook. It is
the same useFormShowToast, already bound.
import { useFormShowToast } from "react-form-actions/sonner"sonner is an optional peer dependency. It is only pulled in when you import
this subpath.
The action state
The hook expects a state object shaped like this (a superset is fine):
type FormToastState = {
message: React.ReactNode
status?: number // 200 means success
toastDuration?: number // override the toast duration in ms
}Defaults: success toasts last 3000 ms, error toasts stay until dismissed.
Writing an adapter
The adapter is tiny:
import type { FormToastAdapter } from "react-form-actions/use-form-show-toast"
const adapter: FormToastAdapter = {
success: (message, options) => {
/* show a success toast, honor options?.duration */
},
error: (message, options) => {
/* show an error toast, honor options?.duration */
},
}Tree-shaking
The package sets "sideEffects": false and ships one file per hook. Import the
barrel and a modern bundler drops the rest:
import { usePreventFormReset } from "react-form-actions"Or import a single hook by its subpath to be explicit:
import { usePreventFormReset } from "react-form-actions/use-prevent-form-reset"The react-form-actions/sonner subpath is the only one that touches sonner.
The barrel never imports it, so the barrel never pulls sonner into your
bundle.
License
MIT © Artur Sedlukha
