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

fields from useForm carries old errors after successful submission + redirect #396

Open
kenn opened this issue Jan 30, 2024 · 5 comments
Open

Comments

@kenn
Copy link

kenn commented Jan 30, 2024

Describe the bug and the expected behavior

With a typical server-side validation in Post/Redirect/Get pattern, but I'm getting the stale error message after redirectWithSuccess (to the same URL with GET). It's persistent until I run full page reload with cmd+R.

export async function action({ request }) {
  const userId = await requireUserId(request)
  const formData = await request.formData()
  const submission = parse(formData, { schema })
  if (submission.intent !== 'submit' || !submission.value) {
    return jsonWithError(submission, 'Failed to update')
  }
  await User.update(userId, submission.value)
  return redirectWithSuccess('', 'Updated!')
}

export default function Page() {
  const { user } = useLoaderData()
  const lastSubmission = useActionData()
  const [form, fields] = useForm({ defaultValue: user, lastSubmission })

  return <>
    ...
    <p>{fields.name.error}</p>
  </>
}
console.log(fields.name.error)
// => 'String must contain at least 2 character(s)'

Conform version

v0.9.1

Steps to Reproduce the Bug or Issue

  1. Put the code above
  2. Enter invalid input first and get an error
  3. Fix the input and submit, redirect successfully
  4. Error message is still there, also check the console
console.log(fields.name.error)
// => 'String must contain at least 2 character(s)'

What browsers are you seeing the problem on?

Chrome

Screenshots or Videos

No response

Additional context

No response

@edmundhung
Copy link
Owner

This is because Remix does not unmount the page and re-initialize the page if you are redirecting to the same URL and Conform has no idea of the action result. You can either follow the remix approach as explained here or simply return the submission object with payload overrided to null:

export async function action({ request }) {
  // ...

  return json({
    ...submission,
    payload: null
  });
}

@kenn
Copy link
Author

kenn commented Jan 31, 2024

Hmm, it's not that I'd like to clear the form inputs, but to clear the error state in the form.

I tried your suggestion:

export async function action({ request }) {
  if (error) {
    return json({ ...submission, payload: null })
  }
}

and upon error, it reverted to the pre-edit value but it didn't show the error state at all.

Also, actionData is not available on the final redirect as it is basically a GET request.


I'm taking the progressive enhancement approach, so I only use Conform in routes that requires nested object access. In other places I still use plain old Zod schema, and this code works pretty well.

import { redirectWithSuccess } from 'remix-toast'

export async function action({ request, params }) {
  const userId = await requireUserId(request)
  const formData = await request.formData()
  const payload = Object.fromEntries(formData)
  const result = UserSchema.safeParse(payload)
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors }
  }
  await User.update(userId, result.data)
  return redirectWithSuccess('', 'User updated!')
}

So I hope there's a way to do the same using Conform.

@edmundhung
Copy link
Owner

edmundhung commented Jan 31, 2024

I would love to have this supported out of the box too! But I just don't have a viable solution at the moment.

The issue here is that Conform has no idea about what happened outside of the form event. The only way it reacts to external event is through lastSubmission (or lastResult on v1). When a redirection happens, lastSubmission will become undefined due to missing actionData but it's unlikely reliable as a signal to reset the form due to several reasons:

  1. When you have multiple forms on the same route, not all your submissions are related to the same form, you don't want the form to be reacting to action data that's not relevant.
  2. When you just start out the form, your lastSubmission is undefined, if you submit the form successfully and then immediately redirect, the lastSubmission is still undefined (No change)

My current suggestion is to not doing a redirection in this case and just return the submission with payload null as the updated action data, or, if you can figure out when a redirection happens, you can set up a useEffect and call formElement.reset() manually:

function Exmaple() {
  const isRedirect = /* ... */

  useEffect(() => {
    if (isRedirect) {
       formRef.current?.reset();
    }
  }, [isRedirect]);

 // ...
}

@kenn
Copy link
Author

kenn commented Feb 2, 2024

Friend pointed me in the right direction and found a solution with submission.reply with Conform v1.0!

https://twitter.com/techtalkjp/status/1752886169912901679

So I removed useForm and all related changes from the component, now the entire code lives in action and looks like this:

import { jsonWithError, redirectWithSuccess } from 'remix-toast'

export async function action({ request, params }) {
  const submission = parseWithZod(await request.formData(), { schema })
  if (submission.status !== 'success') {
    return jsonWithError(submission.reply(), 'Failed to update')
  }
  await User.update(userId, submission.value)
  return redirectWithSuccess('', 'User updated!')
}

The component / client side looks exactly the same as pre-Conform implementation (just print actionData?.error if exists, this gets cleared after redirect), so this is a perfect 1st step toward progressive enhancement.

Also, both jsonWithError and redirectWithSuccess using remix-toast works perfectly.

I would appreciate if you could consider updating the tutorial accordingly — a lot of users from Rails / Laravel would first expect Post/Get/Redirect pattern, and this will be the smallest first step to work with Conform.

@ngbrown
Copy link
Contributor

ngbrown commented Feb 14, 2024

Using an empty object fallback seems to be working for me:

  export async function action({ request }) {
    const userId = await requireUserId(request)
    const formData = await request.formData()
    const submission = parse(formData, { schema })
    if (submission.intent !== 'submit' || !submission.value) {
      return jsonWithError(submission, 'Failed to update')
    }
    await User.update(userId, submission.value)
    return redirectWithSuccess('', 'Updated!')
  }

+ const emptyResult = {};
+
  export default function Page() {
    const { user } = useLoaderData()
    const lastSubmission = useActionData()
-   const [form, fields] = useForm({ defaultValue: user, lastSubmission })
+   const [form, fields] = useForm({ defaultValue: user ?? emptyResult, lastSubmission })

    return <>
      ...
      <p>{fields.name.error}</p>
    </>
  }

The object object has to be created at a module level otherwise React keeps trying to re-render the component.

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

3 participants