Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected navigation can occur with Remix useFetcher and resetForm: true #730

Open
aust1nz opened this issue Aug 3, 2024 · 3 comments
Open

Comments

@aust1nz
Copy link
Contributor

aust1nz commented Aug 3, 2024

Describe the bug and the expected behavior

I've noticed some funny interplay between using Remix's useFetcher and conform's resetForm option, specifically when the useFetcher hits an action from another route. I've created a sandbox that shows this issue here:

https://codesandbox.io/p/devbox/interesting-hamilton-xt5pwn

Specifically, if the user fills out a name and hits enter to submit the form, they're brought to the JSON response that the action returns. Interestingly, if the user clicks the submit button, the form works as expected.

Conform version

1.1.5

Steps to Reproduce the Bug or Issue

https://codesandbox.io/p/devbox/interesting-hamilton-xt5pwn

Alternatively, create a basic Remix app with zod, @conform-to/react, and @conform-to/zod installed.

Update routes/index.tsx

import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { json, type MetaFunction } from "@remix-run/node";
import { useActionData, useLoaderData } from "@remix-run/react";
import { useFetcher } from "react-router-dom";
import { z } from "zod";
import { action as createUserAction } from "./create-user";

export const users = [{ name: "Austin" }];
const schema = z.object({
  name: z.string().min(2),
});

export const meta: MetaFunction = () => {
  return [
    { title: "New Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

export async function loader() {
  return json(users);
}

export default function Index() {
  const users = useLoaderData<typeof loader>();
  const fetcher = useFetcher<typeof createUserAction>();
  const lastResult = useActionData<typeof createUserAction>();
  const [form, fields] = useForm({
    lastResult: fetcher.state === "idle" ? fetcher?.data : null,
    constraint: getZodConstraint(schema),
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
    onValidate({ formData }) {
      return parseWithZod(formData, { schema });
    },
    defaultValue: {
      name: "",
    },
  });

  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <fetcher.Form method="post" action="/create-user" {...getFormProps(form)}>
        <p>
          This example uses a Remix fetcher to submit data to another route. If
          you click "Submit", you'll see things work as expected. The route is
          accessed, and returns a submission.result that clears the input.
        </p>
        <p>
          But if you fill in a value in the input and hit Enter, you're
          unexpectedly navigated to the remote route, which returns JSON.
        </p>
        <label>User Name</label>
        <br />
        <input {...getInputProps(fields.name, { type: "text" })} />
        {fields.name.errors && <div>{fields.name.errors[0]}</div>}
        <br /> <input type="submit" value="Submit" />
      </fetcher.Form>
      {users.map((user, index) => (
        <div key={index}>{user.name}</div>
      ))}
    </div>
  );
}

Create routes/create-user.tsx

import { parseWithZod } from "@conform-to/zod";
import { ActionFunctionArgs } from "@remix-run/node";
import { users } from "./_index";
import { z } from "zod";

const schema = z.object({
  name: z.string().min(4),
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const submission = parseWithZod(formData, { schema });
  if (submission.status !== "success") {
    return submission.reply();
  }

  users.push({
    name: submission.value.name,
  });
  return submission.reply({
    resetForm: true,
  });
}

What browsers are you seeing the problem on?

Chrome

Screenshots or Videos

No response

Additional context

I suppose it's possible this is a Remix issue, but seems to specifically happen when the submission.response({ resetForm: true }) option is configured.

@edmundhung
Copy link
Owner

Thanks for the codesandbox! I can reproduce the issue without Conform here.

Here is what happened:

Conform manages all uncontrolled inputs through a key. When form reset happens, it forces react to unmount all inputs by updating the key passed to them. If your cursor was focusing on one of the inputs, it will then lose the focus and trigger form validation by making a form submission with the validate intent. Normally, Remix will just capture the submit event and trigger a fetch request for you. But it becomes a document request in this case...

It's hard to tell what's really went wrong here because we are indeed abusing the keys right now 😅

Luckily, we are getting rid of the keys soon! I am planning to get #729 released in 1.2.0. As we will no longer re-mount the inputs on form reset, it will not try to validate the form again and so the issue should be gone.

@edmundhung
Copy link
Owner

This is now working properly with v1.2.1. Thanks again!

@aust1nz
Copy link
Contributor Author

aust1nz commented Sep 15, 2024

Awesome, thanks for the update!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants