reactjstypescriptreact-routerreact-router-dommantine

react-router-dom actions and mantine useForm conflict?


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


Solution

  • 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:

    ...
    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;
    }