├── .nvmrc
├── .eslintignore
├── .prettierignore
├── .husky
├── commit-msg
└── prepare-commit-msg
├── .vscode
├── extensions.json
└── settings.json
├── apps
└── example
│ ├── .eslintrc.json
│ ├── src
│ ├── app
│ │ ├── globals.css
│ │ ├── favicon.ico
│ │ ├── todos
│ │ │ ├── add-todo-validation.ts
│ │ │ ├── page.tsx
│ │ │ ├── add-todo-action.ts
│ │ │ └── add-todo-form.tsx
│ │ ├── login
│ │ │ ├── login-validation.ts
│ │ │ ├── page.tsx
│ │ │ ├── login-action.ts
│ │ │ └── login-form.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── lib
│ │ └── safe-action.ts
│ ├── next.config.mjs
│ ├── postcss.config.mjs
│ ├── .gitignore
│ ├── tailwind.config.ts
│ ├── tsconfig.json
│ ├── package.json
│ ├── LICENSE
│ └── pnpm-lock.yaml
├── commitlint.config.js
├── packages
└── adapter-react-hook-form
│ ├── src
│ ├── index.types.ts
│ ├── index.ts
│ ├── hooks.types.ts
│ ├── standard-schema.ts
│ └── hooks.ts
│ ├── .prettierrc.json
│ ├── tsup.config.ts
│ ├── tsconfig.json
│ ├── .eslintrc.js
│ ├── LICENSE
│ ├── package.json
│ ├── release.config.cjs
│ └── README.md
├── .gitignore
├── turbo.json
├── pnpm-workspace.yaml
├── LICENSE
├── .github
└── workflows
│ └── cicd.yml
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.js
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.md
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | pnpm exec commitlint --edit "${1}"
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | exec < /dev/tty && pnpm exec cz --hook || true
--------------------------------------------------------------------------------
/apps/example/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "root": true
4 | }
5 |
--------------------------------------------------------------------------------
/apps/example/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | };
4 |
--------------------------------------------------------------------------------
/apps/example/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/next-safe-action/adapter-react-hook-form/HEAD/apps/example/src/app/favicon.ico
--------------------------------------------------------------------------------
/apps/example/src/lib/safe-action.ts:
--------------------------------------------------------------------------------
1 | import { createSafeActionClient } from "next-safe-action";
2 |
3 | export const ac = createSafeActionClient();
4 |
--------------------------------------------------------------------------------
/apps/example/src/app/todos/add-todo-validation.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const addTodoSchema = z.object({
4 | content: z.string().min(1).max(100),
5 | });
6 |
--------------------------------------------------------------------------------
/apps/example/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | authInterrupts: true,
5 | },
6 | };
7 |
8 | export default nextConfig;
9 |
--------------------------------------------------------------------------------
/apps/example/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/src/index.types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Props for `mapToHookFormErrors`. Also used by the hooks.
3 | */
4 | export type ErrorMapperProps = {
5 | joinBy?: string;
6 | };
7 |
--------------------------------------------------------------------------------
/apps/example/src/app/login/login-validation.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const loginSchema = z.object({
4 | username: z.string().min(3).max(30),
5 | password: z.string().min(8).max(100),
6 | });
7 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "printWidth": 120,
4 | "useTabs": true,
5 | "arrowParens": "always",
6 | "tabWidth": 2,
7 | "semi": true,
8 | "singleQuote": false,
9 | "quoteProps": "consistent"
10 | }
11 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | entry: ["src/index.ts", "src/hooks.ts"],
5 | bundle: true,
6 | format: ["esm"],
7 | clean: true,
8 | splitting: false,
9 | sourcemap: true,
10 | dts: true,
11 | });
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": ["typescript", "typescriptreact"],
3 | "editor.codeActionsOnSave": {
4 | "source.organizeImports": "explicit",
5 | "source.fixAll.eslint": "explicit"
6 | },
7 | "editor.rulers": [120],
8 | "[markdown]": {
9 | "editor.formatOnSave": false
10 | },
11 | "typescript.tsdk": "node_modules/typescript/lib"
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 |
6 | node_modules
7 | yarn.lock
8 | dist
9 | .env*
10 | *.pem
11 | .npmrc
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Exclude .example files
18 | !*.example
19 |
20 | # ESLint
21 | .eslintcache
22 |
23 | # TypeScript stuff
24 | *.tsbuildinfo
25 |
26 | # Turborepo
27 | .turbo
28 |
29 | /packages/adapter-react-hook-form/src/test.ts
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "stream",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": [".next/**", "!.next/cache/**", "dist/**", "build/**"]
8 | },
9 | "lint": {},
10 | "deploy": {
11 | "dependsOn": ["build", "lint"],
12 | "env": ["NPM_TOKEN", "GITHUB_TOKEN"]
13 | },
14 | "dev": {
15 | "cache": false,
16 | "persistent": true
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - apps/*
4 | catalog:
5 | '@hookform/resolvers': ^5.0.1
6 | '@types/node': ^22
7 | '@types/react': ^19
8 | '@types/react-dom': ^19
9 | next-safe-action: 8.0.0
10 | postcss: ^8
11 | react-hook-form: ^7.56.4
12 | tailwindcss: ^3
13 | typescript: 5.8.2
14 | next: 15.3.3
15 | eslint-config-next: 15.3.3
16 | react: ^19
17 | react-dom: ^19
18 | eslint: ^8.57.0
19 | onlyBuiltDependencies:
20 | - esbuild
21 | - sharp
22 | - unrs-resolver
23 |
--------------------------------------------------------------------------------
/apps/example/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "next-safe-action/react-hook-form adapter",
9 | };
10 |
11 | export default function RootLayout({
12 | children,
13 | }: Readonly<{
14 | children: React.ReactNode;
15 | }>) {
16 | return (
17 |
18 |
{children}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/example/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { LoginForm } from "./login-form";
3 |
4 | export default function LoginPage() {
5 | return (
6 |
7 |
8 |
9 | ← Go back to home
10 |
11 |
Login Form
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/apps/example/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/apps/example/src/app/todos/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { getTodos } from "./add-todo-action";
3 | import { AddTodoForm } from "./add-todo-form";
4 |
5 | export default async function TodosPage() {
6 | const todos = await getTodos();
7 |
8 | return (
9 |
10 |
11 |
12 | ← Go back to home
13 |
14 |
Todos Form
15 |
16 |
Current todos from server: {JSON.stringify(todos, null, 1)}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/example/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function HomePage() {
4 | return (
5 |
6 | Examples
7 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "CommonJS",
5 | "lib": ["ES2022"],
6 | "skipLibCheck": true,
7 | "sourceMap": true,
8 | "outDir": "./dist",
9 | "moduleResolution": "node",
10 | "removeComments": false,
11 | "strict": true,
12 | "strictPropertyInitialization": false,
13 | "useUnknownInCatchVariables": false,
14 | "forceConsistentCasingInFileNames": true,
15 | "noImplicitAny": true,
16 | "noImplicitThis": true,
17 | "noImplicitReturns": true,
18 | "noEmitOnError": true,
19 | "noUncheckedIndexedAccess": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "allowSyntheticDefaultImports": true,
22 | "esModuleInterop": true,
23 | "resolveJsonModule": true,
24 | "incremental": false,
25 | "noEmit": true
26 | },
27 | "exclude": ["node_modules"],
28 | "include": ["tsup.config.ts", "./src/**/*.ts"]
29 | }
30 |
--------------------------------------------------------------------------------
/apps/example/src/app/login/login-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { ac } from "@/lib/safe-action";
4 | import { returnValidationErrors } from "next-safe-action";
5 | import { unauthorized } from "next/navigation";
6 | import { loginSchema } from "./login-validation";
7 |
8 | export const loginAction = ac
9 | .inputSchema(loginSchema)
10 | .action(async ({ parsedInput }) => {
11 | if (
12 | parsedInput.username !== "admin" ||
13 | parsedInput.password !== "password"
14 | ) {
15 | await new Promise((resolve) => setTimeout(resolve, 1000));
16 | unauthorized();
17 | returnValidationErrors(loginSchema, {
18 | _errors: ["Invalid username or password"],
19 | username: {
20 | _errors: ["Invalid username"],
21 | },
22 | password: {
23 | _errors: ["Invalid password"],
24 | },
25 | });
26 | }
27 |
28 | return {
29 | successful: true,
30 | username: parsedInput.username,
31 | };
32 | });
33 |
--------------------------------------------------------------------------------
/apps/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@apps/example",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "catalog:",
13 | "@next-safe-action/adapter-react-hook-form": "workspace:*",
14 | "next": "catalog:",
15 | "next-safe-action": "catalog:",
16 | "react": "catalog:",
17 | "react-dom": "catalog:",
18 | "react-hook-form": "catalog:",
19 | "zod": "^3.25.0"
20 | },
21 | "devDependencies": {
22 | "@types/node": "catalog:",
23 | "@types/react": "catalog:",
24 | "@types/react-dom": "catalog:",
25 | "eslint": "catalog:",
26 | "eslint-config-next": "catalog:",
27 | "postcss": "catalog:",
28 | "tailwindcss": "catalog:",
29 | "typescript": "catalog:"
30 | },
31 | "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247"
32 | }
33 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const { defineConfig } = require("eslint-define-config");
3 |
4 | module.exports = defineConfig({
5 | root: true,
6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended-type-checked", "prettier"],
7 | plugins: ["@typescript-eslint", "react-hooks"],
8 | parser: "@typescript-eslint/parser",
9 | parserOptions: {
10 | project: "./tsconfig.json",
11 | tsconfigRootDir: __dirname,
12 | },
13 | ignorePatterns: ["**/*.js", "**/*.mjs", "**/*.cjs", "dist/**"],
14 | rules: {
15 | "@typescript-eslint/consistent-type-imports": "error",
16 | "@typescript-eslint/consistent-type-exports": "error",
17 | "@typescript-eslint/unbound-method": "off",
18 | "@typescript-eslint/ban-ts-comment": "off",
19 | "@typescript-eslint/no-redundant-type-constituents": "off",
20 | "@typescript-eslint/no-explicit-any": "off",
21 | "@typescript-eslint/ban-types": "off",
22 | "react-hooks/exhaustive-deps": "warn",
23 | "@typescript-eslint/require-await": "off",
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/apps/example/src/app/todos/add-todo-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { ac } from "@/lib/safe-action";
4 | import { returnValidationErrors } from "next-safe-action";
5 | import { revalidatePath } from "next/cache";
6 | import { addTodoSchema } from "./add-todo-validation";
7 |
8 | const todos = ["first todo"];
9 |
10 | export async function getTodos() {
11 | return todos;
12 | }
13 |
14 | async function addTodo(newTodoContent: string) {
15 | todos.push(newTodoContent);
16 | return todos;
17 | }
18 |
19 | export const addTodoAction = ac
20 | .inputSchema(addTodoSchema)
21 | .action(async ({ parsedInput }) => {
22 | // Simulate a slow server
23 | await new Promise((resolve) => setTimeout(resolve, 1000));
24 |
25 | if (parsedInput.content === "bad word") {
26 | returnValidationErrors(addTodoSchema, {
27 | content: {
28 | _errors: ["The bad word is not allowed, please remove it"],
29 | },
30 | });
31 | }
32 |
33 | await addTodo(parsedInput.content);
34 |
35 | revalidatePath("/todos");
36 |
37 | return {
38 | successful: true,
39 | content: parsedInput.content,
40 | };
41 | });
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Edoardo Ranghieri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/example/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Edoardo Ranghieri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Edoardo Ranghieri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/cicd.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - beta
8 | pull_request:
9 | branches:
10 | - "*"
11 |
12 | jobs:
13 | ci:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: pnpm/action-setup@v3
18 | with:
19 | version: 9
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: "20"
24 | cache: "pnpm"
25 | - run: pnpm install --frozen-lockfile
26 | - run: pnpm run lint:lib
27 |
28 | cd:
29 | if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }}
30 | runs-on: ubuntu-latest
31 | needs: [ci]
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: pnpm/action-setup@v3
35 | with:
36 | version: 9
37 | - name: Setup Node.js
38 | uses: actions/setup-node@v4
39 | with:
40 | node-version: "20"
41 | cache: "pnpm"
42 | - run: pnpm install --frozen-lockfile
43 | - run: pnpm run build:lib
44 | - name: Release
45 | env:
46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | run: pnpm run deploy:lib
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adapter-react-hook-form-monorepo",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "Monorepo for next-safe-action react-hook-form adapter.",
6 | "scripts": {
7 | "prepare": "is-ci || husky",
8 | "ex": "turbo run dev --filter=@apps/example",
9 | "lint": "turbo run lint",
10 | "build": "turbo run build",
11 | "lint:lib": "turbo run lint --filter=@next-safe-action/adapter-react-hook-form",
12 | "build:lib": "turbo run build --filter=@next-safe-action/adapter-react-hook-form",
13 | "deploy:lib": "turbo run deploy --filter=@next-safe-action/adapter-react-hook-form",
14 | "build:ex": "turbo run build --filter=@apps/eample --force"
15 | },
16 | "author": "Edoardo Ranghieri",
17 | "license": "MIT",
18 | "engines": {
19 | "node": ">=18.17"
20 | },
21 | "config": {
22 | "commitizen": {
23 | "path": "./node_modules/cz-conventional-changelog"
24 | }
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/TheEdoRan/next-safe-action.git"
29 | },
30 | "dependencies": {
31 | "@commitlint/cli": "^19.3.0",
32 | "@commitlint/config-conventional": "^19.2.2",
33 | "commitizen": "^4.3.0",
34 | "cz-conventional-changelog": "^3.3.0",
35 | "husky": "^9.0.11",
36 | "is-ci": "^3.0.1",
37 | "turbo": "^2.4.4",
38 | "typescript": "catalog:"
39 | },
40 | "packageManager": "pnpm@10.6.5"
41 | }
42 |
--------------------------------------------------------------------------------
/apps/example/src/app/todos/add-todo-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import {
5 | useHookFormOptimisticAction
6 | } from "@next-safe-action/adapter-react-hook-form/hooks";
7 | import { addTodoAction } from "./add-todo-action";
8 | import { addTodoSchema } from "./add-todo-validation";
9 |
10 | type Props = {
11 | todos: string[];
12 | };
13 |
14 | export function AddTodoForm({ todos }: Props) {
15 | const { form, action, handleSubmitWithAction, resetFormAndAction } =
16 | useHookFormOptimisticAction(addTodoAction, zodResolver(addTodoSchema), {
17 | actionProps: {
18 | currentState: { todos },
19 | updateFn: (state, input) => {
20 | return {
21 | todos: [...state.todos, input.content],
22 | };
23 | },
24 | },
25 | formProps: {
26 | mode: "onChange",
27 | },
28 | });
29 |
30 | return (
31 |
32 |
60 |
61 |
62 | Optimistic state todos:{" "}
63 | {JSON.stringify(action.optimisticState.todos, null, 1)}
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/apps/example/src/app/login/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
5 | import { loginAction } from "./login-action";
6 | import { loginSchema } from "./login-validation";
7 |
8 | export function LoginForm() {
9 | const { form, action, handleSubmitWithAction, resetFormAndAction } =
10 | useHookFormAction(loginAction, zodResolver(loginSchema), {
11 | formProps: {
12 | mode: "onChange",
13 | },
14 | actionProps: {
15 | onSuccess: (args) => {
16 | console.log("onSuccess called:", args);
17 | window.alert("Logged in successfully!");
18 | resetFormAndAction();
19 | },
20 | onNavigation: (args) => {
21 | console.log("onNavigation called:", args);
22 | },
23 | onSettled: (args) => {
24 | console.log("onSettled called:", args);
25 | },
26 | },
27 | });
28 |
29 | return (
30 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
2 |
3 | import type { ValidationErrors } from "next-safe-action";
4 | import type { FieldError, FieldErrors } from "react-hook-form";
5 | import type {} from "zod";
6 | import type { ErrorMapperProps } from "./index.types";
7 | import type { InferOutputOrDefault, StandardSchemaV1 } from "./standard-schema";
8 |
9 | /**
10 | * Maps a validation errors object to an object of `FieldErrors` compatible with react-hook-form.
11 | * You should only call this function directly for advanced use cases, and prefer exported hooks.
12 | */
13 | export function mapToHookFormErrors(
14 | validationErrors: ValidationErrors | undefined,
15 | props?: ErrorMapperProps
16 | ) {
17 | if (!validationErrors || Object.keys(validationErrors).length === 0) {
18 | return undefined;
19 | }
20 |
21 | const fieldErrors: FieldErrors> = {};
22 |
23 | function mapper(ve: Record, paths: string[] = []) {
24 | // Map through validation errors.
25 | for (const key of Object.keys(ve)) {
26 | // If validation error is an object, recursively call mapper so we go one level deeper
27 | // at a time. Pass the current paths to the mapper as well.
28 | if (typeof ve[key] === "object" && ve[key] && !Array.isArray(ve[key])) {
29 | mapper(ve[key], [...paths, key]);
30 | }
31 |
32 | // We're just interested in the `_errors` field, which must be an array.
33 | if (key === "_errors" && Array.isArray(ve[key])) {
34 | // Initially set moving reference to root `fieldErrors` object.
35 | let ref = fieldErrors as Record;
36 |
37 | // Iterate through the paths, create nested objects if needed and move the reference.
38 | for (let i = 0; i < paths.length - 1; i++) {
39 | const p = paths[i]!;
40 | ref[p] ??= {};
41 | ref = ref[p];
42 | }
43 |
44 | // The actual path is the last one. If it's undefined, it means that we're at the root level.
45 | const path = paths.at(-1) ?? "root";
46 |
47 | // Set the error for the current path.
48 | ref[path] = {
49 | type: "validate",
50 | message: ve[key].join(props?.joinBy ?? " "),
51 | } as FieldError;
52 | }
53 | }
54 | }
55 |
56 | mapper(validationErrors ?? {});
57 | return fieldErrors;
58 | }
59 |
60 | export type * from "./index.types";
61 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/src/hooks.types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-function-type */
2 |
3 | import type { SafeActionFn } from "next-safe-action";
4 | import type { HookCallbacks, UseActionHookReturn, UseOptimisticActionHookReturn } from "next-safe-action/hooks";
5 | import type { UseFormProps, UseFormReturn } from "react-hook-form";
6 | import type { ErrorMapperProps } from "./index.types";
7 | import type { InferInputOrDefault, InferOutputOrDefault, StandardSchemaV1 } from "./standard-schema";
8 |
9 | /**
10 | * Optional props for `useHookFormAction` and `useHookFormOptimisticAction`.
11 | */
12 | export type HookProps = {
13 | errorMapProps?: ErrorMapperProps;
14 | actionProps?: HookCallbacks;
15 | formProps?: Omit, FormContext, InferOutputOrDefault>, "resolver">;
16 | };
17 |
18 | /**
19 | * Type of the return object of the `useHookFormAction` hook.
20 | */
21 | export type UseHookFormActionHookReturn<
22 | ServerError,
23 | S extends StandardSchemaV1 | undefined,
24 | CVE,
25 | Data,
26 | FormContext = any,
27 | > = {
28 | action: UseActionHookReturn;
29 | form: UseFormReturn, FormContext, InferOutputOrDefault>;
30 | handleSubmitWithAction: (e?: React.BaseSyntheticEvent) => Promise;
31 | resetFormAndAction: () => void;
32 | };
33 |
34 | /**
35 | * Type of the return object of the `useHookFormOptimisticAction` hook.
36 | */
37 | export type UseHookFormOptimisticActionHookReturn<
38 | ServerError,
39 | S extends StandardSchemaV1 | undefined,
40 | CVE,
41 | Data,
42 | State,
43 | FormContext = any,
44 | > = Omit, "action"> & {
45 | action: UseOptimisticActionHookReturn;
46 | };
47 |
48 | /**
49 | * Infer the type of the return object of the `useHookFormAction` hook.
50 | */
51 | export type InferUseHookFormActionHookReturn =
52 | T extends SafeActionFn
53 | ? UseHookFormActionHookReturn
54 | : never;
55 |
56 | /**
57 | * Infer the type of the return object of the `useHookFormOptimisticAction` hook.
58 | */
59 | export type InferUseHookFormOptimisticActionHookReturn =
60 | T extends SafeActionFn
61 | ? UseHookFormOptimisticActionHookReturn
62 | : never;
63 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@next-safe-action/adapter-react-hook-form",
3 | "version": "0.0.0-development",
4 | "private": false,
5 | "description": "This adapter offers a way to seamlessly integrate next-safe-action with react-hook-form.",
6 | "main": "./dist/index.mjs",
7 | "module": "./dist/index.mjs",
8 | "types": "./dist/index.d.mts",
9 | "files": [
10 | "dist"
11 | ],
12 | "exports": {
13 | ".": "./dist/index.mjs",
14 | "./hooks": "./dist/hooks.mjs"
15 | },
16 | "typesVersions": {
17 | "*": {
18 | ".": [
19 | "./dist/index.d.mts"
20 | ],
21 | "hooks": [
22 | "./dist/hooks.d.mts"
23 | ]
24 | }
25 | },
26 | "funding": [
27 | {
28 | "type": "github",
29 | "url": "https://github.com/sponsors/TheEdoRan"
30 | },
31 | {
32 | "type": "paypal",
33 | "url": "https://www.paypal.com/donate/?hosted_button_id=ES9JRPSC66XKW"
34 | }
35 | ],
36 | "scripts": {
37 | "lint": "tsc && prettier --write . && eslint .",
38 | "build": "tsup",
39 | "deploy": "semantic-release"
40 | },
41 | "keywords": [
42 | "next",
43 | "nextjs",
44 | "react",
45 | "rsc",
46 | "react server components",
47 | "mutation",
48 | "action",
49 | "actions",
50 | "react actions",
51 | "next actions",
52 | "server actions",
53 | "next-safe-action",
54 | "next safe action",
55 | "react-hook-form",
56 | "react hook form",
57 | "react forms"
58 | ],
59 | "author": "Edoardo Ranghieri",
60 | "license": "MIT",
61 | "publishConfig": {
62 | "access": "public"
63 | },
64 | "devDependencies": {
65 | "@eslint/js": "^9.9.0",
66 | "@hookform/resolvers": "catalog:",
67 | "@manypkg/cli": "^0.21.4",
68 | "@types/node": "catalog:",
69 | "@types/react": "catalog:",
70 | "@types/react-dom": "catalog:",
71 | "@typescript-eslint/eslint-plugin": "^8.27.0",
72 | "@typescript-eslint/parser": "^8.27.0",
73 | "eslint": "catalog:",
74 | "eslint-config-prettier": "^9.1.0",
75 | "eslint-define-config": "^2.1.0",
76 | "eslint-plugin-react-hooks": "^4.6.2",
77 | "next-safe-action": "catalog:",
78 | "prettier": "^3.3.3",
79 | "react": "catalog:",
80 | "react-hook-form": "catalog:",
81 | "semantic-release": "^23.0.8",
82 | "tsup": "^8.2.4",
83 | "tsx": "^4.17.0",
84 | "typescript": "catalog:",
85 | "typescript-eslint": "^7.18.0",
86 | "zod": "^3.25.0"
87 | },
88 | "peerDependencies": {
89 | "@hookform/resolvers": ">= 5.0.0",
90 | "next": ">= 14.0.0",
91 | "next-safe-action": ">= 8.0.0",
92 | "react": ">= 18.2.0",
93 | "react-dom": ">= 18.2.0",
94 | "react-hook-form": ">= 7.0.0"
95 | },
96 | "repository": {
97 | "type": "git",
98 | "url": "https://github.com/next-safe-action/adapter-react-hook-form.git"
99 | },
100 | "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247"
101 | }
102 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/release.config.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('semantic-release').GlobalConfig}
3 | */
4 | module.exports = {
5 | branches: [
6 | {
7 | name: "main",
8 | },
9 | {
10 | name: "next",
11 | channel: "next",
12 | prerelease: true,
13 | },
14 | {
15 | name: "experimental",
16 | channel: "experimental",
17 | prerelease: true,
18 | },
19 | {
20 | name: "beta",
21 | channel: "beta",
22 | prerelease: true,
23 | },
24 | {
25 | name: "4.x",
26 | range: "4.x",
27 | channel: "4.x",
28 | },
29 | ],
30 | plugins: [
31 | [
32 | "@semantic-release/commit-analyzer",
33 | {
34 | preset: "conventionalcommits",
35 | releaseRules: [
36 | {
37 | breaking: true,
38 | release: "major",
39 | },
40 | {
41 | revert: true,
42 | release: "patch",
43 | },
44 | {
45 | type: "feat",
46 | release: "minor",
47 | },
48 | {
49 | type: "fix",
50 | release: "patch",
51 | },
52 | {
53 | type: "perf",
54 | release: "patch",
55 | },
56 | {
57 | type: "refactor",
58 | release: "patch",
59 | },
60 | {
61 | type: "build",
62 | release: "patch",
63 | },
64 | {
65 | type: "docs",
66 | release: "patch",
67 | },
68 | {
69 | type: "chore",
70 | release: false,
71 | },
72 | {
73 | type: "test",
74 | release: false,
75 | },
76 | {
77 | type: "ci",
78 | release: false,
79 | },
80 | {
81 | type: "style",
82 | release: false,
83 | },
84 | ],
85 | parserOpts: {
86 | noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"],
87 | },
88 | },
89 | ],
90 | [
91 | "@semantic-release/release-notes-generator",
92 | {
93 | preset: "conventionalcommits",
94 | presetConfig: {
95 | types: [
96 | {
97 | type: "revert",
98 | section: "Reverts",
99 | hidden: false,
100 | },
101 | {
102 | type: "feat",
103 | section: "Features",
104 | hidden: false,
105 | },
106 | {
107 | type: "fix",
108 | section: "Bug Fixes",
109 | hidden: false,
110 | },
111 | {
112 | type: "perf",
113 | section: "Performance improvements",
114 | hidden: false,
115 | },
116 | {
117 | type: "refactor",
118 | section: "Refactors",
119 | hidden: false,
120 | },
121 | {
122 | type: "build",
123 | section: "Build System",
124 | hidden: false,
125 | },
126 | {
127 | type: "docs",
128 | section: "Documentation",
129 | hidden: false,
130 | },
131 | {
132 | type: "chore",
133 | hidden: true,
134 | },
135 | {
136 | type: "test",
137 | hidden: true,
138 | },
139 | {
140 | type: "ci",
141 | hidden: true,
142 | },
143 | {
144 | type: "style",
145 | hidden: true,
146 | },
147 | ],
148 | },
149 | parserOpts: {
150 | noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"],
151 | },
152 | },
153 | ],
154 | "@semantic-release/npm",
155 | "@semantic-release/github",
156 | ],
157 | };
158 |
--------------------------------------------------------------------------------
/packages/adapter-react-hook-form/src/standard-schema.ts:
--------------------------------------------------------------------------------
1 | /** The Standard Schema interface. */
2 | export interface StandardSchemaV1 {
3 | /** The Standard Schema properties. */
4 | readonly "~standard": StandardSchemaV1.Props;
5 | }
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-namespace
8 | export declare namespace StandardSchemaV1 {
9 | /** The Standard Schema properties interface. */
10 | export interface Props {
11 | /** The version number of the standard. */
12 | readonly version: 1;
13 | /** The vendor name of the schema library. */
14 | readonly vendor: string;
15 | /** Validates unknown input values. */
16 | readonly validate: (value: unknown) => Result