edmundhung / conform

A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
https://conform.guide
MIT License
1.85k stars 101 forks source link

Double POST with validate/__intent__ ? #133

Closed meza closed 1 year ago

meza commented 1 year ago

Using Remix, I'm finding a weird behaviour for a simple newsletter form.

When I submit, I get this:

image

The first call behaves as expected image

It does a redirect('/');

The next invocation however looks like this:

image

This obviously fails on the server that's expecting a different intent, thus renders the form invalid - returning the data, filling out the form.

What am I doing wrong? Where does the validate/intent come from?

My form config:


const [form, { email }] = useForm({
    constraint: getFieldsetConstraint(schema),
    fallbackNative: true,
    lastSubmission: lastSubmission,
    shouldValidate: 'onBlur',
    onValidate: function ({ formData }) {
      const result = parse(formData, { schema: schema });
      setIsValid(result.error === undefined);
      return result;
    }
  });

Some of the other bits:


const NEWSLETTER_INTENT = 'newsletter.signup';

export const schema = z.object({
  email: z.string().min(1, 'validation.email.required').email('validation.email.invalid')
});

export const newsletterSignUpAction: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const submission = parse(formData, { schema: schema });

  console.log({submission: submission});

  if (!submission.value || submission.intent !== NEWSLETTER_INTENT) {
    return json(submission, { status: 400 });
  }

  throw redirect('/');
  // do something with result.data
};
edmundhung commented 1 year ago

Hi @meza, I noticed you are using a custom intent (NEWSLETTER_INTENT). How are you configuring the submit button? It would be even better if you can show me the whole component with your newsletter form.

meza commented 1 year ago

It's a bit difficult to show because it's all out in different components + in a private repo but here's the button:

import { useEffect, useState } from 'react';
import { conform } from '@conform-to/react';
import classNames from 'classnames';

export interface SubmitButtonProps {
  intent: string;
  className?: string;
  disabled?: boolean;
}

export const SubmitButton = (props: SubmitButtonProps) => {
  const [disabled, setDisabled] = useState(props.disabled || false);

  useEffect(() => {
    setDisabled(props.disabled || false);
  }, [props.disabled]);

  return (
    <button className={classNames('primary', props.className)} type='submit' name={conform.INTENT} value={props.intent} disabled={disabled}>Submit</button>
  );
};

The input as follows:

export interface InputProps {
  label?: string;
  placeholder: string;
  conformField: FieldConfig;
  tabIndex?: number | undefined;
  type?: HTMLInputTypeAttribute | undefined;
  value?: string | undefined;
}
export const Input = (props: InputProps) => {
  const finalType = props.type || 'text';
  const id = useId();
  const errorId = useId();
  const { t } = useTranslation();

  return (
    <div className={classNames('input')}>
      <input
        className={classNames({ 'error': props.conformField.error !== undefined })}
        {...conform.input(props.conformField, { type: finalType })}
        id={id}
        placeholder={' '}
        tabIndex={props.tabIndex}
        aria-invalid={props.conformField.error === undefined ? 'false' : 'true'}
        aria-required={props.conformField.required === undefined ? false : props.conformField.required}
        aria-describedby={errorId}
      />
      {props.label && <label htmlFor={id} className={classNames({ 'required': props.conformField.required })}>{props.label}</label>}
      <div id={errorId} role='alert' className={classNames('input-error')} aria-live={'polite'}>{props.conformField.error ? t(props.conformField.error) : ''}</div>
    </div>
  );
};

And the form is:

<newsletter.Form
        className={classNames('newsletter', { 'visible': true })}
        method={'POST'} {...form.props}>
        <Input label={'What is your email address?'} type={'email'} tabIndex={3} conformField={email}/>
        <div className={'button'}>
          <SubmitButton disabled={!!email.error || (newsletter.state === 'submitting')} intent={NEWSLETTER_INTENT}/>
        </div>
</newsletter.Form>
edmundhung commented 1 year ago

It looks like there is a bug on the revalidation logic currently which tries to validate the submit button šŸ˜… , very likely when you unfocus the submit button (i.e. onBlur). But this still doesn't answer why you saw the request made there with client validation in place.

The only exception I can think of is if it throws an error during validation and so Conform fallbacks to server validation. Can you verify this? (e.g. Conform will print out the error if onValidate throws an error, or you can wrap it with a try-catch block)

meza commented 1 year ago

@edmundhung about to test! Been tied up with other things but let's experiment!

meza commented 1 year ago

It looks like there is a bug on the revalidation logic currently which tries to validate the submit button šŸ˜… , very likely when you unfocus the submit button (i.e. onBlur). But this still doesn't answer why you saw the request made there with client validation in place.

The only exception I can think of is if it throws an error during validation and so Conform fallbacks to server validation. Can you verify this? (e.g. Conform will print out the error if onValidate throws an error, or you can wrap it with a try-catch block)

Verified it, there's no validation error.

Any other ideas I could try?

I also see the double submission on the server side image

Obviously the first one is correct and then the second one fails because I don't have that intent

meza commented 1 year ago

It would appear that it's all coming from the onBlur. When I change it to onInput, it doesn't do the double post. It however doesn't clear out the submitted field but that might just be user error :D

edmundhung commented 1 year ago

Hi @meza, sorry for the late reply. I was busy with Remix Conf for the last couple weeks.

I have a patch ready that avoids the extra validation caused by the button and it will be released by the end of this week. But I still believe there is an unknown issue in your case as I wouldn't expect it doing a server validation with client validation setup. Any chance you can provide a sandbox that reproduce the issue? You can fork the remix example here.

edmundhung commented 1 year ago

This should be fixed on v0.6.2.