Releases: seasonedcc/remix-forms
v4.0.0
Breaking changes
1. Only Zod v4 is supported
Remix Forms v4 removed support for Zod v3. Before upgrading Remix Forms, please upgrade your Zod dependency to v4.
If you cannot upgrade to Zod v4 yet, Remix Forms 3.1.1 is stable and will continue working with Zod v3.
How to upgrade
Update your Zod dependency to v4:
npm install zod@latestMost Remix Forms users won't need to change their code after upgrading Zod. However, if you use advanced Zod features, you may need to make some adjustments. The most common breaking changes in Zod v4 that may affect your schemas are:
Default behavior in optional fields
Zod v4 now applies defaults inside properties even within optional fields. This change aligns better with expectations but may cause breakage if your code relies on key existence.
Before (Zod v3):
const schema = z.object({
name: z.string(),
role: z.string().default('user').optional(),
})
schema.parse({ name: 'Alice' })
// Result: { name: 'Alice' }After (Zod v4):
const schema = z.object({
name: z.string(),
role: z.string().default('user').optional(),
})
schema.parse({ name: 'Alice' })
// Result: { name: 'Alice', role: 'user' }Object methods replaced with top-level functions
The .strict() and .passthrough() methods have been replaced with top-level functions.
Before (Zod v3):
const schema = z.object({ name: z.string() }).strict()
const schema2 = z.object({ name: z.string() }).passthrough()After (Zod v4):
const schema = z.strictObject({ name: z.string() })
const schema2 = z.looseObject({ name: z.string() })Function API changes
If you use z.function(), the API has changed significantly. The result is no longer a Zod schema but a "function factory" for defining Zod-validated functions.
Before (Zod v3):
const myFunction = z.function()
.args(z.string(), z.number())
.returns(z.boolean())After (Zod v4):
const myFunction = z.function(
z.tuple([z.string(), z.number()]),
z.boolean(),
)Array .nonempty() behavior
The .nonempty() method now behaves identically to .min(1). The inferred type does not change, but if you relied on the old behavior for type narrowing, consider using z.tuple() instead.
For a complete list of Zod v4 breaking changes, see the official Zod v4 migration guide.
Minor changes
2. New exports: objectFromSchema and ObjectFromSchema
We now export two new utilities that were previously internal:
objectFromSchema: A function that creates a default object from a Zod schemaObjectFromSchema: A TypeScript type that infers the object type from a schema
These can be useful for creating initial form values:
import { objectFromSchema, type ObjectFromSchema } from 'remix-forms'
import { z } from 'zod'
const schema = z.object({
name: z.string().default(''),
age: z.number().default(0),
})
const initialValues = objectFromSchema(schema)
// Result: { name: '', age: 0 }
type FormData = ObjectFromSchema<typeof schema>
// Type: { name: string; age: number }What's Changed
- Add missing shapeInfo enum test by @danielweinmann in #354
- Add missing mutation error test by @danielweinmann in #356
- Add missing boolean coercion and navigation tests by @danielweinmann in #355
- Add tests for mutation transformResult hooks by @danielweinmann in #357
- Fix flaky zod-effects test by @danielweinmann in #359
- Fix e2e test flakiness by @danielweinmann in #360
- Fix flaky test by mocking router early by @danielweinmann in #361
- Split long e2e tests by @danielweinmann in #362
- Split slow tests into separate files by @danielweinmann in #363
- Split slow tests by @danielweinmann in #365
- Refactor coerceToForm by @danielweinmann in #366
- Split slow e2e tests by @danielweinmann in #367
- Split some e2e specs for parallel test runs by @danielweinmann in #374
- Add Claude instructions by @felipefreitag in #379
- .github/workflows: Migrate workflows to Blacksmith runners by @blacksmith-sh[bot] in #382
- [BREAKING CHANGE] Support Zod4 and drop Zod3 support by @danielweinmann in #383
- Upgrade React Router to 7.9.4 and export objectFromSchema and ObjectFromSchema by @danielweinmann in #384
New Contributors
- @blacksmith-sh[bot] made their first contribution in #382
Full Changelog: v3.1.1...v4.0.0
v3.1.1
What's Changed
- Fix accented labels in selects by @danielweinmann in #351
Full Changelog: v3.1.0...v3.1.1
v3.1.0
What's Changed
- Add autoComplete prop to Field by @danielweinmann in #344
Full Changelog: v3.0.1...v3.1.0
v3.0.1
What's Changed
- Replace eslint packages and prettier with Biomejs by @gustavoguichard in #291
- Upgrade React Router to latest version by @danielweinmann in #297
- Upgrade typescript to latest version by @danielweinmann in #298
- Upgrade React and fix autoFocus prop by @danielweinmann in #299
Full Changelog: v3.0.0...v3.0.1
v3.0.0
Breaking changes
1. Only React Router v7 is supported
Remix Forms v3 removed support for Remix and React Router v6. Before upgrading Remix Forms, please upgrade your app to React Router v7.
The Remix team made upgrading easy, so you probably should. If you can't upgrade, Remix Forms 2.3.0 is stable and will continue working with React Router v6 and Remix.
2. createForm replaced by SchemaForm
Instead of using createForm, you can now import SchemaForm directly from remix-forms.
Before:
import { createForm } from 'remix-forms'
// For Remix, import it like this
import { Form as FrameworkForm, useActionData, useSubmit, useNavigation } from '@remix-run/react'
// For React Router 6.4, like this
import { Form as FrameworkForm, useActionData, useSubmit, useNavigation } from 'react-router-dom'
const Form = createForm({ component: FrameworkForm, useNavigation, useSubmit, useActionData })
export { Form }After:
import { SchemaForm } from 'remix-forms'We stopped using the name Form for our form component to avoid ambiguity with React Router's Form component. We now refer to our forms as "schema forms".
3. createFormAction replaced by formAction
Instead of using createFormAction, you can now import formAction directly from remix-forms.
Before:
import { createFormAction } from 'remix-forms'
// For Remix, import it like this
import { redirect, json } from '@remix-run/node'
// For React Router 6.4, like this
import { redirect, json } from 'react-router-dom'
const formAction = createFormAction({ redirect, json })
export { formAction }After:
import { formAction } from 'remix-forms'4. domain-functions replaced by composable-functions
We now depend on Composable Functions for our mutations. If you are a heavy Domain Functions user, please follow our complete migration guide. For most Remix Forms users, only a few simple changes are needed.
makeDomainFunction becomes applySchema
If you already have mutations created with makeDomainFunction or mdf, use applySchema instead.
Before:
import { makeDomainFunction } from 'domain-functions'
const mutation = makeDomainFunction(schema)(async (values) => (
console.log(values) /* or anything else, like saveMyValues(values) */
))After:
import { applySchema } from 'composable-functions'
const mutation = applySchema(schema)(async (values) => (
console.log(values) /* or anything else, like saveMyValues(values) */
))environment becomes context
Composable Functions renamed environment to context.
Before:
export const action: ActionFunction = async ({ request }) => {
return formAction({
request,
schema,
mutation,
environment: { customHeader: request.headers.get('customHeader') },
})
}After:
export const action = async ({ request }: Route.ActionArgs) => {
return formAction({
request,
schema,
mutation,
context: { customHeader: request.headers.get('customHeader') },
})
}Global errors with Error
Composable Functions requires us to throw an instance of Error for generating global errors. We cannot throw literal strings anymore.
Before:
const mutation = makeDomainFunction(schema)(async (values) => {
if (values.password !== 'supersafe') {
throw 'Wrong email or password'
}
return values
})After:
const mutation = applySchema(schema)(async (values) => {
if (values.password !== 'supersafe') {
throw new Error('Wrong email or password')
}
return values
})The second param to InputError is now an array
We now need to pass the field name as an array when throwing field errors.
Before:
const mutation = makeDomainFunction(schema)(async (values) => {
if (takenEmails.includes(values.email)) {
throw new InputError('Email already taken', 'email')
}
return values
})After:
const mutation = applySchema(schema)(async (values) => {
if (takenEmails.includes(values.email)) {
throw new InputError('Email already taken', ['email'])
}
return values
})5. PerformMutation type renamed to MutationResult
If you are using the PerformMutation type, you'll need to change to MutationResult from now on. The type is the same, only the name has changed.
6. formAction always returns the mutation result
Now we always return the full MutationResult on formAction. If you use formAction without a successPath and manually get the action data in your components, you'll have to check for success first and only then access the mutation result inside data:
export const action = async ({ request }: Route.ActionArgs) =>
formAction({ request, schema, mutation })
export default function Component({ actionData }: Route.ComponentProps) {
if (!actionData) {
return <p>Our action has not run yet.</p>
}
if (!actionData.success) {
return (
<div>
<h3>Errors:</h3>
<pre>{JSON.stringify(actionData.errors, null, 2)}</pre>
<h3>Values:</h3>
<pre>{JSON.stringify(actionData.values, null, 2)}</pre>
</div>
)
}
return (
<div>
<h3>Data:</h3>
<pre>{JSON.stringify(actionData.data, null, 2)}</pre>
</div>
)
}7. formAction returns 422 status on failed mutations
To avoid revalidating loaders unnecessarily, formAction now returns a 422 status on failed mutations.
8. Removed beforeAction and beforeSuccess callbacks
We removed the beforeAction and beforeSuccess callbacks from formAction in favor of transformedResult, described below.
9. Removed Callback type
Because we don't have callbacks anymore, we removed the Callback type that was previously exported.
10. Removed deprecated parseActionData
We removed parseActionData from our schema form component, which was marked as deprecated since v2.
11. onTransition renamed to onNavigation
SchemaForm prop onTransition was renamed to onNavigation to keep up with React Router's new terminology.
Minor changes
12. Added transformResult
Instead of relying on callbacks, we now offer a more flexible transformResult option to performMutation and formAction. It is a function that receives a MutationResult and returns another result. Here's the type definition:
transformResult?: (
result: MutationResult<Schema, D>,
) => MutationResult<Schema, D> | Promise<MutationResult<Schema, D>>A common use case is to make conditional redirects with custom headers, like:
export const action = async ({ request }: Route.ActionArgs) =>
formAction({
request,
schema,
mutation,
successPath: '/success',
transformResult: (result) => {
if (!result.success) {
session.flash('error', 'Invalid username/password')
throw redirect('/login', {
headers: { 'Set-Cookie': await commitSession(session) },
})
}
return result
},
})The above example will throw a redirect in case of failures and return the result otherwise.
13. successPath now accepts async functions
You can now pass an async function to successPath on formAction, like:
export const action = async ({ request }: Route.ActionArgs) =>
formAction({
request,
schema,
mutation,
successPath: async data => await calculateRedirectPath(data),
})Full Changelog: v2.3.0...v3.0.0
v2.3.0
What's Changed
- Add navigate, fetcherKey, and unstable_flushSync props to Form by @danielweinmann in #240
Full Changelog: v2.2.1...v2.3.0
v2.2.1
What's Changed
- Ensure action data errors are not shown in fetcher forms by @danielweinmann in #234
Full Changelog: v2.2.0...v2.2.1
v2.2.0
v2.1.0
What's Changed
- Add emptyOptionLabel prop to Form by @danielweinmann in #223
Full Changelog: v2.0.0...v2.1.0
v2.0.0
🔥 Breaking changes
- We now depend on domain-functions
2.x.
What's Changed
- Upgrade domain-functions to v2 by @danielweinmann in #222
Full Changelog: v1.6.6...v2.0.0