Remix
better-form integrates smoothly with Remix, allowing you to build multi-step forms while leveraging Remix's data loading and actions.
better-form is a client-side library. Use it within client components and combine it with Remix actions for server-side form handling.
Installation
npm install @better_form/coreSetup
1. Import Styles
Add styles to your root route:
import type { LinksFunction } from '@remix-run/node';
import styles from '@better_form/core/styles?url';
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: styles },
];
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}Note the ?url suffix for CSS imports in Remix. This tells Vite to handle it as a URL import.
2. Create Your Form Component
import { WizardProvider, WizardContainer, defaultFieldComponents } from '@better_form/core';
import type { WizardConfig } from '@better_form/core';
const config: WizardConfig = {
id: 'contact-form',
steps: [
{
id: 'info',
title: 'Your Information',
fieldGroups: [
{
id: 'personal',
fields: [
{
id: 'name',
name: 'name',
label: 'Full Name',
type: 'text',
validation: { required: true },
},
{
id: 'email',
name: 'email',
label: 'Email Address',
type: 'email',
validation: { required: true },
},
],
},
],
},
{
id: 'message',
title: 'Your Message',
fieldGroups: [
{
id: 'content',
fields: [
{
id: 'subject',
name: 'subject',
label: 'Subject',
type: 'text',
validation: { required: true },
},
{
id: 'message',
name: 'message',
label: 'Message',
type: 'textarea',
validation: { required: true },
},
],
},
],
},
],
};
interface ContactFormProps {
onSubmit: (data: Record<string, unknown>) => Promise<void>;
}
export function ContactForm({ onSubmit }: ContactFormProps) {
return (
<WizardProvider
config={config}
fieldComponents={defaultFieldComponents}
onSubmit={onSubmit}
>
<WizardContainer />
</WizardProvider>
);
}3. Use with Remix Actions
import type { ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
import { useActionData, useSubmit } from '@remix-run/react';
import { ContactForm } from '~/components/ContactForm';
// Server-side action
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.json();
// Validate and process on server
// e.g., send email, save to database
// Return success or redirect
return redirect('/contact/success');
}
export default function ContactPage() {
const submit = useSubmit();
const actionData = useActionData<typeof action>();
const handleSubmit = async (data: Record<string, unknown>) => {
submit(data, {
method: 'POST',
encType: 'application/json',
});
};
return (
<main className="container">
<h1>Contact Us</h1>
<ContactForm onSubmit={handleSubmit} />
</main>
);
}Complete Multi-Step Example
Here's a full registration flow with Remix:
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
import { useActionData, useLoaderData, useSubmit, useNavigation } from '@remix-run/react';
import { WizardProvider, WizardContainer, defaultFieldComponents } from '@better_form/core';
import type { WizardConfig } from '@better_form/core';
// Load any necessary data (e.g., country list)
export async function loader({ request }: LoaderFunctionArgs) {
return json({
countries: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'it', label: 'Italy' },
],
});
}
// Handle form submission
export async function action({ request }: ActionFunctionArgs) {
const data = await request.json();
// Server-side validation
if (!data.email || !data.password) {
return json({ error: 'Email and password are required' }, { status: 400 });
}
// Create user account
// const user = await createUser(data);
return redirect('/dashboard');
}
export default function RegisterPage() {
const { countries } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const submit = useSubmit();
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
const config: WizardConfig = {
id: 'registration',
steps: [
{
id: 'account',
title: 'Account',
fieldGroups: [
{
id: 'credentials',
fields: [
{
id: 'email',
name: 'email',
label: 'Email',
type: 'email',
validation: { required: true },
},
{
id: 'password',
name: 'password',
label: 'Password',
type: 'password',
validation: { required: true, minLength: 8 },
},
],
},
],
},
{
id: 'profile',
title: 'Profile',
fieldGroups: [
{
id: 'personal',
fields: [
{
id: 'name',
name: 'name',
label: 'Full Name',
type: 'text',
validation: { required: true },
},
{
id: 'country',
name: 'country',
label: 'Country',
type: 'select',
options: countries,
},
],
},
],
},
],
};
const handleSubmit = async (data: Record<string, unknown>) => {
submit(data, {
method: 'POST',
encType: 'application/json',
});
};
return (
<main>
<h1>Create Account</h1>
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<WizardProvider
config={config}
fieldComponents={defaultFieldComponents}
onSubmit={handleSubmit}
>
<WizardContainer />
</WizardProvider>
{isSubmitting && <p>Creating your account...</p>}
</main>
);
}Error Handling
Handle server-side validation errors and display them in your form:
import { useActionData } from '@remix-run/react';
export async function action({ request }: ActionFunctionArgs) {
const data = await request.json();
const errors: Record<string, string> = {};
if (!data.email?.includes('@')) {
errors.email = 'Please enter a valid email';
}
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
// Process valid submission
return redirect('/success');
}
export default function ContactPage() {
const actionData = useActionData<typeof action>();
const submit = useSubmit();
// Display errors from server
return (
<div>
{actionData?.errors && (
<div className="error-banner">
{Object.entries(actionData.errors).map(([field, message]) => (
<p key={field}>{message}</p>
))}
</div>
)}
<ContactForm onSubmit={(data) => submit(data, { method: 'POST', encType: 'application/json' })} />
</div>
);
}Troubleshooting
CSS Import Issues
If styles aren't loading, try using a CSS side effect import:
import '@better_form/core/styles';Or use the links function with the correct path resolution.
Hydration Mismatches
better-form is client-side only. If you experience hydration issues, ensure you're not trying to render the form on the server without proper client boundaries.
TypeScript Configuration
Ensure your tsconfig.json is properly configured:
{
"compilerOptions": {
"moduleResolution": "bundler",
"esModuleInterop": true,
"jsx": "react-jsx"
}
}