jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.97k stars 2.79k forks source link

A way to keep backend errors with client validation #150

Open vladshcherbin opened 7 years ago

vladshcherbin commented 7 years ago

Here is a simple form with errors example:

name - no error
surname - no error

When I submit form, I receive backend errors: { surname: 'Exists' }. So, I set field errors and now they are:

name - no error
surname - 'Exists'

Now, if I change or blur the value of the name field, the whole form validation will be triggered and backend error will be erased for surname field.

How is it possible to erase field's backend error only if the value of that field was changed?

I can think of possible solution with using form's status to save backend errors, checking the field's previous/new value, but it feels like a really big hack.

jaredpalmer commented 7 years ago

2 options:

  1. Write an small function that transforms your backend error into same shape as formik’s errors and then call setErrors

  2. Use status setStatus

vladshcherbin commented 7 years ago

@jaredpalmer yes, I transformed backend errors and the fields get them. The problem is that when you change/blur one of fields, all other backend errors are gone since newly triggered validation will erase them.

jaredpalmer commented 7 years ago

We use option 1, and pass around a small helper function.

vladshcherbin commented 7 years ago

@jaredpalmer here is a codesandbox demo of what I mean.

Set the values of email and username, submit the form and you'll receive 2 backend errors. Now, change the value or blur one of the fields (to trigger validation) - all backend errors will be gone.

jaredpalmer commented 7 years ago

Hmmm yeah not ideal.

Need to think about this one.

vladshcherbin commented 7 years ago

Possible solution - add submitErrors object where you store submit errors. This way, submitErrors won't interact with client-side errors and can be handled separately.

amankkg commented 7 years ago

Currently we handle such errors via setStatus. In our case such errors don't prevent form from being re-submitted and they're not cleared away even after related field's onBlur/onChange event.

But in general case, after setting such errors from outside - each field should hold its own errors until onBlur/onChange event.

upd: it's hard to achieve this with yup validation schema (e.g. f(schema, values) => errors), since now validation would also depend on props/status f(schema, values, formikStatus) => errors

jaredpalmer commented 7 years ago

@VladShcherbin this use case is tough, and I think the best way to deal with this is to actually have two separate feedback components that render error messages for each input. First attempt, show regular error/touched errors. If there are then API errors, keep them in status.errors. Then show those instead during the second attempt. That way, regular validation won’t overwrite them.

vladshcherbin commented 7 years ago

@jaredpalmer nah, other libraries can handle this use case.

I'm sure, we'll find a nice way to deal with this too 😉

klis87 commented 7 years ago

@amankkg I had the same problem. I use setStatus to set server side errors, but I wanted those errors to disappear after a given field is changed. Here is my solution:

<Field
  type="email"
  name="email"
  onChange={(v) => {
    if (status && status.email) {
      const { email, ...rest } = status;
      setStatus(rest);
    }
    handleChange(v);
  }}
/>

If this pattern was repeated, you could reuse similar logic in your custom Field component.

jariz commented 7 years ago

workaround ≠ solution This is a rather really common use case and I think Formik could solve this in a much neater way. I think this issue should be reopened.

jaredpalmer commented 7 years ago

Feel free to submit PR’s open to exploring this

stale[bot] commented 6 years ago

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

vladshcherbin commented 6 years ago

yep, instead of fixing/adding things let's just close issues like nothing happened. not even surprised to see this bot here 😄

codepunkt commented 6 years ago

@vladshcherbin thanks!

jaredpalmer commented 6 years ago

@vladshcherbin I wish I had time to go through it all but I don't. So rather than continue to let things pile up, I figured I could make small dent with this bot. If people still need help they will holler (just like you just did!).

This feature is planned in v2 btw #828

stale[bot] commented 6 years ago

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

stale[bot] commented 5 years ago

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

jaredpalmer commented 5 years ago

We were just talking about this today internally.

jaredpalmer commented 5 years ago

There are 2 parts to this:

  1. catching submit errors (which also requires async onSubmit)
  2. storing backend errors and displaying them to the user

For 2, as described, you can use status to store the data and then in your render function figure out what to show. FWIW status used to be called error (singular) and was intended for this use case.

It's awkward in v1, but will be less so in v2:

function Fieldset({ name }) {
  const { status } = useFormikContext()
  const [field, meta] = useField(name)
  const apiError = getIn(status, name);
  return <>
    <input {...field} />
    {meta.touch && (meta.error || apiError)  && <div>{meta.error || apiError}</div>}
  </>
}

However, if we implement async submit AND error handling in v2, we would need to introduce a new key so as to not mess with people's existing usage of status.

fivethreeo commented 5 years ago

Is this possible in v2 now?

deklanw commented 5 years ago

@fivethreeo @jaredpalmer wondering this too

jp94 commented 5 years ago

I am also wondering if this is possible in v2. Does anyone have a good workaround for v1, if I am to only care for server-side validation? I have a workaround by using setFieldError during handleSubmit and resetting individual field errors during field's onChange..

PorridgeBear commented 5 years ago

For anyone else coming to this thread and being none the wiser, I found @jaredpalmer gave an answer on SO that solved my wondering how this is done (in v1).

https://stackoverflow.com/questions/52986962/how-to-properly-use-formiks-seterror-method-react-library

Snippet:

onSubmit={(values, { setSubmitting, setErrors }) => {
                        const axios = getAxiosInstance();

                        axios.post('/reset/', {
                            'email': email
                          }).then((response) => {
                            setSubmitting(false);
                          }).catch((error) => {
                            setSubmitting(false);
                            const fieldErrorsFromResponse = error.response.data.field_errors;

                            // fieldErrorsFromResponse from server has {'field_errors': {'email': 'Invalid'}}
                            if (fieldErrorsFromResponse !== null){
                              setErrors(fieldErrorsFromResponse);
                            }
                          }
                        );
                      }}
                      render={({ isSubmitting, errors }) => (

                        <Form className="form-horizontal">
                          <p>Enter the email address associated with your account, and we'll email you a link to reset your password.</p>
                          <FormGroup>
                            <Label htmlFor="username">Email address</Label>
                            <Field type="email" name="email" />
                            <ErrorMessage name="email" component="div" className="field-error" />
                          </FormGroup>
donaldpipowitch commented 5 years ago

If someone needs a bigger example for copy'n'pasting the idea from @jaredpalmer mentioned in https://github.com/jaredpalmer/formik/issues/150#issuecomment-447147835 here is a gist:

https://gist.github.com/donaldpipowitch/4f3989edb2aadd9e44c2856c65e90b2e

Here you can find two hooks to retrieve and reset server side errors properly. The hooks either look for field specific errors (setStatus({ field: 'Your error message' }) or global errors (setStatus(true)).

t-nunes commented 5 years ago

I made an example that can make life easier for some people. follow the link more this looks like a hack

import { useCallback } from "react";
import { useField as useFieldFormik, useFormikContext, getIn } from "formik";

export function useField(name) {
  const { status, setStatus } = useFormikContext();
  const [{ onBlur: onBlurFormik, ...field }, meta] = useFieldFormik(name);
  const apiError = getIn(status, name);

  const onBlurMemo = useCallback(
    e => {
      setStatus({
        ...status,
        [name]: null
      });
      onBlurFormik(e);
    },
    [status, name, setStatus, onBlurFormik]
  );

  return [{ onBlur: onBlurMemo, ...field }, { ...meta, apiError }];
}
import React from "react";
import { useField } from "./use-field";

function Input(props) {
  const [field, meta] = useField(props.name);

  return (
    <>
      <input {...field} {...props} />
      {(meta.error || meta.apiError) && (
        <div>{meta.error || meta.apiError}</div>
      )}
    </>
  );
}

export default Input;
import React from "react";
import { Formik } from "formik";
import Input from "./Input";
import { request } from "./helpers";

function FormExample() {
  return (
    <Formik
      validate={values => {
        let errors = {};
        if (!values.email) {
          errors.email = "Required by client";
        }

        if (!values.password) {
          errors.email = "Required by client";
        }

        return errors;
      }}
      initialValues={{ email: "", password: "" }}
      onSubmit={async (values, actions) => {
        try {
          await request();
        } catch (error) {
          actions.setStatus(error);
        }
      }}
    >
      {props => (
        <form onSubmit={props.handleSubmit} autoComplete="off">
          <label htmlFor="email" style={{ display: "block" }}>
            Email
          </label>
          <Input
            id="email"
            name="email"
            placeholder="Enter your email"
            type="text"
            autoComplete="off"
          />

          <label htmlFor="password" style={{ display: "block" }}>
            Password
          </label>
          <Input
            id="password"
            name="password"
            placeholder="Enter your password"
            type="password"
            autoComplete="off"
          />

          <button type="submit">Submit</button>
        </form>
      )}
    </Formik>
  );
}

export default FormExample;

https://codesandbox.io/s/formik-api-errors-9xwjs

demeralde commented 4 years ago

Anyone know the best way to handle this with sagas? My API is decoupled from the form's submit function, so I can't directly access the errors/response in Formik.onSubmit.

My form calls a submit function with the form's values:

<Formik
  initialValues={{
    email,
    password: "",
    passwordConfirmation: ""
  }}
  validationSchema={ValidationSchema}
  onSubmit={(values, { setErrors, setSubmitting }) => {
    const { password, passwordConfirmation } = values;
    submit(token, password, passwordConfirmation);
  }}
>

submit dispatches an action that starts a request:

const mapDispatchToProps = dispatch => ({
  submit: (token, password, passwordConfirmation) => {
    dispatch(setPasswordRequest({ token, password, passwordConfirmation }));
  }
});

There's then a "loading", "success", and "failure" action that corresponds to setPasswordRequest. The errors and loading state are managed in the Redux store.

But in order to set a form's errors and loading state with Formik, you need to use setErrors and setSubmitting from the Formik.onSubmit prop.

I could pass setErrors and setSubmitting to setPasswordRequest and call them in the saga, but that seems messy. Anyone know the best way to approach this?

ivankoleda commented 4 years ago

Here is what work weel for me. Quite similar to @t-nunes example, but without changing statuses until next submission.

Examples for both versions

danielbyun commented 4 years ago

@ivankoleda Thank you for your examples, I was able to implement Formik with server-side validation. Just one minor thing tho, a little bit of inconsistency in the dirty variable.

At times with the error, the dirty variable will be set to true so that the user can reset the form, but other times it will not set it to true so the user has to reset the form one by one.

How do I fix this issue? Is there a workaround?

Thank you in advance.

ivankoleda commented 4 years ago

@danielbyun dirty variable and Reset button hasn't been removed after forking default formik codesandbox they are redundant here. However it has no impact on validation. If I missed something please describe some real life case or create sandbox where described behavior creates some inconsistencies.

chetanyakan commented 4 years ago

Is there any way to use this with formik-material-ui?

shaxaaa commented 3 years ago

@chetanyakan

you can set status in submit handler like this setStatus({ error: error._error, apiErrors: error.errors });

then use @ivankoleda code for error

const getError = (name, { touched, errors, status }) => {
  const fieldTouched = getIn(touched, name);
  const backendError = getIn(status, ["apiErrors", name]);
  const clientError = getIn(errors, name);

  if (clientError && fieldTouched) {
    return clientError;
  }

  if (backendError && !fieldTouched) {
    return backendError;
  }

  return undefined;
};

then you have two options:

  1. Use a wrapper element for each formik-material-ui component and pass the error and helpertext depending on the result of getError
  2. Do it only for some of the components. For example you only want to display the email in use error:
    const emailError = getError("email", {
    touched,
    errors,
    status,
    });

and pass the email error to the component like this:

<Field
  type="email"
  name="email"
  component={TextField}
  label={"Email"}
  error={!!emailError}
  helperText={emailError}
/>