Is there a conlict between react-router-dom
actions and useForm
? For example, I'm exploring an Authentication form from mantine. I want to leverage react-router-dom actions. When I replace form with Form from react-router-dom
and trigger the action by clicking Register
nothing is stored in request.formData
. This can be fixed by adding name="email"
into the TextInput
component. However, I noticed that all the form validation functionality disappoears. I suspect this is due to replacing mantine's form
with react-router-dom
's Form
. Was wondering if there is an elegant way to use react-router-dom
actions with Mantine's useForm
?
index.tsx
import { createTheme, MantineProvider } from "@mantine/core";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import AuthenticationForm, { action as authenticationAction } from "./authform";
const router = createBrowserRouter([
{
path: "",
element: <AuthenticationForm />,
action: authenticationAction,
},
]);
const theme = createTheme({
/** Put your mantine theme override here */
});
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement); // createRoot(container!) if you use TypeScript
root.render(
<MantineProvider theme={theme}>
<RouterProvider router={router}>
<App />
</RouterProvider>
</MantineProvider>
);
app.tsx
import "@mantine/core/styles.css";
import { MantineProvider } from "@mantine/core";
import AuthenticationForm from "./authform";
export default function App() {
return (
<MantineProvider>
<AuthenticationForm />
</MantineProvider>
);
}
authForm.tsx
import { useToggle, upperFirst } from "@mantine/hooks";
import { useForm } from "@mantine/form";
import {
TextInput,
PasswordInput,
Text,
Paper,
Group,
PaperProps,
Button,
Divider,
Checkbox,
Anchor,
Stack,
} from "@mantine/core";
import { ActionFunctionArgs, Form } from "react-router-dom";
// import { GoogleButton } from './GoogleButton';
// import { TwitterButton } from './TwitterButton';
export default function AuthenticationForm(props: PaperProps) {
const [type, toggle] = useToggle(["login", "register"]);
const form = useForm({
initialValues: {
email: "",
name: "",
password: "",
terms: true,
},
validate: {
email: (val) => (/^\S+@\S+$/.test(val) ? null : "Invalid email"),
password: (val) =>
val.length <= 6
? "Password should include at least 6 characters"
: null,
},
});
return (
<Paper radius="md" p="xl" withBorder {...props}>
<Text size="lg" fw={500}>
Welcome to Mantine, {type} with
</Text>
<Group grow mb="md" mt="md">
{/* <GoogleButton radius="xl">Google</GoogleButton>
<TwitterButton radius="xl">Twitter</TwitterButton> */}
</Group>
<Divider label="Or continue with email" labelPosition="center" my="lg" />
<Form method="post">
<Stack>
{type === "register" && (
<TextInput
label="Name"
placeholder="Your name"
value={form.values.name}
onChange={(event) =>
form.setFieldValue("name", event.currentTarget.value)
}
radius="md"
/>
)}
<TextInput
required
label="Email"
placeholder="hello@mantine.dev"
value={form.values.email}
onChange={(event) =>
form.setFieldValue("email", event.currentTarget.value)
}
error={form.errors.email && "Invalid email"}
radius="md"
/>
<PasswordInput
required
label="Password"
placeholder="Your password"
value={form.values.password}
onChange={(event) =>
form.setFieldValue("password", event.currentTarget.value)
}
error={
form.errors.password &&
"Password should include at least 6 characters"
}
radius="md"
/>
{type === "register" && (
<Checkbox
label="I accept terms and conditions"
checked={form.values.terms}
onChange={(event) =>
form.setFieldValue("terms", event.currentTarget.checked)
}
/>
)}
</Stack>
<Group justify="space-between" mt="xl">
<Anchor
component="button"
type="button"
c="dimmed"
onClick={() => toggle()}
size="xs"
>
{type === "register"
? "Already have an account? Login"
: "Don't have an account? Register"}
</Anchor>
<Button type="submit" radius="xl">
{upperFirst(type)}
</Button>
</Group>
</Form>
</Paper>
);
}
// export async function action({ params }: ActionFunctionArgs) {
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
console.log("Auth action");
const email = formData.get("email");
const password = formData.get("password");
console.log({ email, password });
return null;
}
Here is a sandbox
The React-Router-DOM Form
component is handling submitting the form outside/separately from any form validation/handling that Mantine might be trying to do. You can use the useFetcher
hook to manually submit the form after Mantine has had a chance to apply any submission validation.
Example:
useFetcher
hook from React-Router-DOMform
element and onSubmit
propform.submit
to handle the form validation and submissionval.length < 6
to match error message that it needs to be at least 6 characters long...
import { useForm } from "@mantine/form";
...
import { useFetcher, ActionFunctionArgs } from "react-router-dom";
export default function AuthenticationForm(props: PaperProps) {
const fetcher = useFetcher();
...
const form = useForm({
initialValues: {
email: "",
name: "",
password: "",
terms: true,
},
validate: {
email: (val) => (/^\S+@\S+$/.test(val) ? null : "Invalid email"),
password: (val) =>
val.length < 6
? "Password should include at least 6 characters"
: null,
},
});
return (
<Paper radius="md" p="xl" withBorder {...props}>
...
<form
onSubmit={form.onSubmit((data) => {
// form data validated by the time we hit here, submit form
fetcher.submit(data, { method: "POST" });
})}
>
...
</form>
</Paper>
);
}
Full Code:
...
import { useForm } from "@mantine/form";
...
import { useFetcher, ActionFunctionArgs } from "react-router-dom";
export default function AuthenticationForm(props: PaperProps) {
const fetcher = useFetcher();
const [type, toggle] = useToggle(["login", "register"]);
const form = useForm({
initialValues: {
email: "",
name: "",
password: "",
terms: true,
},
validate: {
email: (val) => (/^\S+@\S+$/.test(val) ? null : "Invalid email"),
password: (val) =>
val.length < 6
? "Password should include at least 6 characters"
: null,
},
});
return (
<Paper radius="md" p="xl" withBorder {...props}>
<Text size="lg" fw={500}>
Welcome to Mantine, {type} with
</Text>
...
<Divider label="Or continue with email" labelPosition="center" my="lg" />
<form
onSubmit={form.onSubmit((data) => {
// form data validated by the time we hit here, submit form
fetcher.submit(data, { method: "POST" });
})}
>
<Stack>
{type === "register" && (
<TextInput
label="Name"
placeholder="Your name"
value={form.values.name}
onChange={(event) =>
form.setFieldValue("name", event.currentTarget.value)
}
radius="md"
/>
)}
<TextInput
required
label="Email"
placeholder="hello@mantine.dev"
value={form.values.email}
onChange={(event) =>
form.setFieldValue("email", event.currentTarget.value)
}
error={form.errors.email && "Invalid email"}
radius="md"
/>
<PasswordInput
required
label="Password"
placeholder="Your password"
value={form.values.password}
onChange={(event) =>
form.setFieldValue("password", event.currentTarget.value)
}
error={
form.errors.password &&
"Password should include at least 6 characters"
}
radius="md"
/>
{type === "register" && (
<Checkbox
label="I accept terms and conditions"
checked={form.values.terms}
onChange={(event) =>
form.setFieldValue("terms", event.currentTarget.checked)
}
/>
)}
</Stack>
<Group justify="space-between" mt="xl">
<Anchor
component="button"
type="button"
c="dimmed"
onClick={() => toggle()}
size="xs"
>
{type === "register"
? "Already have an account? Login"
: "Don't have an account? Register"}
</Anchor>
<Button type="submit" radius="xl">
{upperFirst(type)}
</Button>
</Group>
</form>
</Paper>
);
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
console.log("Auth action");
const email = formData.get("email");
const password = formData.get("password");
console.log({ email, password });
return null;
}