# Migrating from 0.6 to 1.0

1.0 is a security and correctness release. Most apps need four mechanical changes (requirements, toast provider, form actions used with `useFormAction`, native-form schemas); the rest are behaviour changes to review.

Projects that stay on `^0.6.0` are not affected: npm never installs a new major version on its own.

## 1. Requirements

| | 0.6 | 1.0 |
|---|---|---|
| Next.js | 14–16 | 15 or 16 (App Router) |
| React | 18 or 19 | 19 |
| Zod | bundled dependency | peer dependency: `npm install zod` |
| sonner | required by the hooks | not required (toast adapter) |
| react-hook-form, @hookform/resolvers | required by `/hooks` | optional, `>= 7.55` and `>= 5.1`, only for `/hooks/form` |
| TypeScript | any 5.x | 5.4 or later |

## 2. Toasts: add `ActionForgeProvider`, remove `ToastRestorer`

The hooks no longer import sonner. Pass your toast function once, from a Client Component:

```tsx
"use client";
import { toast } from "sonner";
import { ActionForgeProvider } from "next-action-forge/hooks";

export function Providers({ children }: { children: React.ReactNode }) {
  return <ActionForgeProvider toast={toast}>{children}</ActionForgeProvider>;
}
```

Without it, toasts are skipped (with a one-time warning in development). `ToastRestorer` is removed: delete its import and element.

## 3. `useFormAction`: new import path, regular actions

```diff
- import { useFormAction } from "next-action-forge/hooks";
+ import { useFormAction } from "next-action-forge/hooks/form";
```

`useFormAction` now sends the form values as an object to a regular action. Every action used with it changes from `.formAction()` to `.action()`:

```diff
  export const updateUser = adminClient
    .inputSchema(UpdateUserSchema)
-   .formAction(async (input, context) => { … });
+   .action(async (input, context) => { … });
```

Passing a `.formAction()` is now a type error, and at runtime it answers `INVALID_FORM_DATA`. Booleans, numbers and dates arrive with their real types, so schemas written for the old string conversion keep working. Also:

- Field types still come from `defaultValues` (fields like `z.coerce.number()` stay numbers), and an action chosen at runtime (`isEdit ? updateUser : createUser`) keeps working. Explicit generics are now `useFormAction<FieldValues, typeof action>`.
- `transformData(values)` returns the action input (it returned FormData).
- `actionState` is `undefined` until the first submit (it started as `{ success: true, data: undefined }`); `result` holds the same `ActionResult` as `useServerAction`.
- A `FileList` from `register("file")` is sent as `File[]`.

## 4. Native forms: `.formAction()` no longer guesses types

`.formAction()` is for `<form action>` and `useActionState` only. Values stay strings; `"true"`, `"false"`, `"null"`, ISO dates and JSON text are no longer converted. Declare conversions in the schema:

| Field | 0.6 schema | 1.0 schema |
|---|---|---|
| checkbox | `z.boolean()` | `z.stringbool().default(false)` |
| number | `z.number()` | `z.coerce.number()`, with `z.preprocess(emptyToUndefined, …)` when optional |
| date | `z.date()` | `z.iso.date()` (string), or a transform to `Date` |
| nullable text | `z.string().nullable()` | `z.preprocess(emptyToNull, z.string().nullable())` |
| JSON object | JSON string in one field | `address.city`, `items[0].name` field names |

A form action now also works when passed directly to `<form action>`, where React calls it with `(formData)` only.

## 5. Results and statuses

- `ActionResult` has a required `status`: `"success" | "error" | "navigation"`. `hasSucceeded` is now true for void and falsy results.
- A `redirect()` inside an action resolves `execute()` with `{ status: "navigation" }` instead of `{ fetchError: "NEXT_REDIRECT" }`; `notFound()` and friends render Next's boundaries.
- `isRedirecting` returns to `false` once the navigation settles, also for a redirect to the current page. Workarounds that avoided `.redirect()` on updates can go.

## 6. Validation errors

- `fields` keys are full dotted paths (`address.city`, `items.0.name`) instead of the first segment.
- Root-level issues (a `.refine()` on the whole object) are reported in `formErrors`; `fields` is omitted when no field failed (it was `{}`).
- Code reading `error.fields.address` for nested errors must read `error.fields["address.city"]`.

## 7. Errors that no longer reach the client

- A result that fails `.outputSchema()` answers `OUTPUT_VALIDATION_ERROR` (500) without `fields`; `.onError()` and `onFailure` receive an `OutputValidationError`.
- A `ZodError` thrown by the handler (not by input validation) is an internal error. To expose it on purpose: `.onError((e) => e instanceof z.ZodError ? zodErrorToServerActionError(e) : undefined)`.
- Error messages are revealed only when `NODE_ENV` is exactly `"development"` (before, any value other than `"production"` revealed them).
- Unhandled errors are logged with `console.error` in every environment. Pass `createActionClient({ logger })` to route them, or `logger: false` to silence the library.

## 8. Lifecycle hooks and `.onError()`

- Hooks and `.onError()` receive redacted copies: password, token, secret, API key, cookie, `*hash`, card and SSN keys hold `"[REDACTED]"`. The handler still gets the real values. Configure with `createActionClient({ redact })`.
- `input` is `undefined` when validation had not succeeded; the new `rawInput` always holds what the client sent. **Audit hooks that log failed attempts from `input` must read `rawInput`.**
- `.onError()` receives `{ parsedInput, rawInput }`; for input-validation failures `parsedInput` is now `undefined` (it held the raw input).
- `onStart`'s `context` holds what `.useBeforeValidation()` middleware added.
- `fireAndForget` hooks start after the action resolved.

## 9. Middleware

- Middleware passed to `.use()` receives the accumulated context typed, and may return only the keys it adds. `context as AuthContext` casts can go.
- `createTypedMiddleware<Output, Input, RequiredContext>` gained a third type parameter: declare the context the middleware needs and `.use()` checks it.
- A middleware returning anything other than `{ context }` or `{ error: { message } }` fails closed with `INTERNAL_ERROR` (before, `{}` continued and `{ context, error: undefined }` failed).
- New `.useBeforeValidation()` for checks that must run on the raw input first.

## 10. Redirects

- Targets must be same-origin paths starting with `/`. A fixed external target throws when the action module loads; a computed one is dropped and logged. Use `redirect()` from `next/navigation` to leave the app.
- New `{ redirect }` option on `.action()`/`.formAction()`, typed from the result; redirect callbacks may be `async`.
- The implicit `/login` redirect for `AUTHENTICATION_ERROR` is now configurable (`createActionClient({ authRedirect })`), respects a `redirectTo` or `shouldRedirect: false` set by the error, and no longer mutates the object returned by `toServerActionError()`. `authRedirect.to` must be a same-origin path too.

## Checklist

- [ ] `npm install next-action-forge@1 zod` (plus `react-hook-form @hookform/resolvers@^5.1` for forms)
- [ ] Add `<ActionForgeProvider toast={toast}>`, remove `ToastRestorer`
- [ ] Import `useFormAction` from `next-action-forge/hooks/form`; switch its actions to `.action()`
- [ ] Review native-form schemas (`z.stringbool()`, `z.coerce.number()`, `emptyToNull`)
- [ ] Replace reads of nested `error.fields` keys with dotted paths
- [ ] Audit hooks: log failed attempts from `rawInput`
- [ ] Remove `context as …` casts in middleware; consider `.useBeforeValidation()` for authentication
- [ ] Check that `.redirect()` targets are same-origin paths
