Rik Schoonbeek
Rik Schoonbeek

Reputation: 4460

How to properly use Formik's setError method? (React library)

I am using React communicating with a backend. Now trying to properly implement Formik (Form library).

Main question: How do I properly use Formik's setError method?

Client side validation errors show properly, but now I am trying to set/show the backend validation errors, which are returned with a response with status code 400.

Link to the docs on the method I am trying to use

I am using this method in the method named handle400Error in the code below.

My React (and Formik) code:

import React, { Component } from "react";
import axios from "axios";
import { Formik } from "formik";
import * as Yup from "yup";
import styled from "styled-components";
import FormError from "../formError";

const Label = styled.label``;

class LoginForm extends Component {
  initialValues = {
    password: "",
    username: ""
  };

  getErrorsFromValidationError = validationError => {
    const FIRST_ERROR = 0;
    return validationError.inner.reduce((errors, error) => {
      return {
        ...errors,
        [error.path]: error.errors[FIRST_ERROR]
      };
    }, {});
  };

  getValidationSchema = values => {
    return Yup.object().shape({
      password: Yup.string()
        .min(6, "Password must be at least 6 characters long")
        .required("Password is required!"),
      username: Yup.string()
        .min(5, "Username must be at least 5 characters long")
        .max(40, "Username can not be longer than 40 characters")
        .required("Username is required")
    });
  };

  handleSubmit = async (values, { setErrors }) => {
    console.log("handleSubmit");

    try {
      const response = await axios.post(
        "http://127.0.0.1:8000/rest-auth/login/",
        values
      );
      const loginToken = response.data["key"];
      this.handleLoginSuccess(loginToken);
    } catch (exception) {
      // Expected: 400 status code
      if (exception.response && exception.response.status === 400) {
        // Display server validation errors
        this.handle400Error(exception.response.data, setErrors);
      }
      console.log("exception", exception);
      console.log("exception.response", exception.response);
    }
  };

  handle400Error = (backendErrors, setErrors) => {
    let errors = {};
    for (let key in backendErrors) {
      errors[key] = backendErrors[key][0]; // for now only take the first error of the array
    }
    console.log("errors object", errors);
    setErrors({ errors });
  };

  handleUnexpectedError = () => {};

  handleLoginSuccess = loginToken => {
    console.log("handleLoginSuccess");
    this.props.setGreeneryAppState({
      loginToken: loginToken
    });
    this.props.history.replace(`/${this.props.locale}/`);
  };

  validate = values => {
    const validationSchema = this.getValidationSchema(values);
    try {
      validationSchema.validateSync(values, { abortEarly: false });
      return {};
    } catch (error) {
      return this.getErrorsFromValidationError(error);
    }
  };

  render() {
    return (
      <React.Fragment>
        <h1>Login</h1>
        <Formik
          initialValues={this.initialValues}
          validate={this.validate}
          validationSchema={this.validationSchema}
          onSubmit={this.handleSubmit}
          render={({
            errors,
            touched,
            values,
            handleBlur,
            handleChange,
            handleSubmit
          }) => (
            <form onSubmit={handleSubmit}>
              {errors.non_field_errors && (
                <formError>{errors.non_field_errors}</formError>
              )}
              <Label>Username</Label>
              <input
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.username}
                type="text"
                name="username"
                placeholder="Enter username"
              />
              {touched.username &&
                errors.username && <FormError>{errors.username}</FormError>}
              <Label>Password</Label>
              <input
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.password}
                type="password"
                name="password"
                placeholder="Enter password"
              />
              {touched.password &&
                errors.password && <FormError>{errors.password}</FormError>}
              <button type="submit">Log in</button>
            </form>
          )}
        />
      </React.Fragment>
    );
  }

Upvotes: 55

Views: 113915

Answers (6)

wavefly
wavefly

Reputation: 187

This is what you're looking for:

The setErrors method only accepts an object.

setErrors({ username: 'This is a dummy procedure error' });

Upvotes: 4

Youssef El-Shabasy
Youssef El-Shabasy

Reputation: 180

const formik = useFormik({
    initialValues:{
        email:"",password:"",username:""
    },
    validationSchema:validation_schema,
    onSubmit:(values) => {
        const {email,password,username} = values
        // ......
    }
});


formik.setErrors({email:"Is already taken"}) // error message for email field

Upvotes: 6

jaredpalmer
jaredpalmer

Reputation: 1919

Formik author here...

setError was deprecated in v0.8.0 and renamed to setStatus. You can use setErrors(errors) or setStatus(whateverYouWant) in your handleSubmit function to get the behavior you want here like so:

handleSubmit = async (values, { setErrors, resetForm }) => {
   try {
     // attempt API call
   } catch(e) {
     setErrors(transformMyApiErrors(e))
     // or setStatus(transformMyApiErrors(e))
   }
}

What's the difference use setStatus vs. setErrors?

If you use setErrors, your errors will be wiped out by Formik's next validate or validationSchema call which can be triggered by the user typing (a change event) or blurring an input (a blur event). Note: this assumed you have not manually set validateOnChange and validateOnBlur props to false (they are true by default).

IMHO setStatus is actually ideal here because it will place the error message(s) on a separate part of Formik state. You can then decide how / when you show this message to the end user like so.

// status can be whatever you want
{!!status && <FormError>{status}</FormError>}
// or mix it up, maybe transform status to mimic errors shape and then ...
{touched.email && (!!errors.email && <FormError>{errors.email}</FormError>) || (!!status && <FormError>{status.email}</FormError>) }

Be aware that the presence or value of status has no impact in preventing the next form submission. Formik only aborts the submission process if validation fails.

Upvotes: 70

Harshal
Harshal

Reputation: 8310

<Formik
  validationSchema={schema}
  initialValues={{ email: '', pswrd: '' }}
  onSubmit={(values, actions) => {
    // initialise error status <---- 1
    actions.setStatus(undefined); 

    setTimeout(() => {

      // setting error status <---- 2
      actions.setStatus({
        email: 'This is email already exists.',
        pswrd: 'This is password is incorrect',
      });

    }, 500);
  }}
  // destructuring status <---- 3
  render={({ handleSubmit, handleChange, handleBlur, values, errors, status }) => (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="email"
        value={values['email']}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      <input
        type="text"
        name="pswrd"
        value={values['pswrd']}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      <button type="submit">Submit</button>

      // using error status <---- 4
      {status && status.email ? (
        <div>API Error: {status.email}</div>
      ) : (
        errors.email && <div>Validation Error: {errors.email}</div>
      )}

      {status && status.pswrd ? (
        <div>API Error: {status.pswrd}</div>
      ) : (
        errors.pswrd && <div>Validation Error: {errors.pswrd}</div>
      )}
    </form>
  )}
/>

Upvotes: 2

redheadedstep
redheadedstep

Reputation: 333

Another way to handle this situation is to assign a specific key to your api errors and use setStatus for status messages.

__handleSubmit = (values, {setStatus, setErrors}) => {
  return this.props.onSubmit(values)
    .then(() => {
      setStatus("User was updated successfully.");
    })
    .catch((err) => {
      setErrors({api: _.get(err, ["message"])});
    });
}

Then any validation errors would appear by the fields and any api errors could appear at the bottom:

<Formik
  validationSchema={LoginSchema}
  initialValues={{login: ""}}
  onSubmit={this.__handleSubmit}
>
  {({isSubmitting, status, errors, values, setFieldValue}) => (
    <Form className={classNames("form")}>
      <FormGroup>
        <InputGroup>
          <InputGroup.Text>
            <FontAwesomeIcon icon={faUser} fixedWidth />
          </InputGroup.Text>
          <Field
            name="login"
            type={"text"}
            placeholder="Login"
            className="form-control"
          />
        </InputGroup>
        <ErrorMessage name="login" />
      </FormGroup>
      <Button type="submit" variant="primary" disabled={isSubmitting}>
        Submit
      </Button>
      {errors && _.has(errors, ["api"]) && <div className="text-danger">{_.get(errors, ["api"])}</div>}
      {status && <div className="text-success">{status}</div>}
    </Form>
  )}
</Formik>

Don't forget about the schema...

const LoginSchema = Yup.object().shape({
  login: Yup.string()
    .min(4, 'Too Short!')
    .max(70, 'Too Long!')
    .required('Login is required'),
});

The api error message will show until the next validation call by Formik (i.e. the user is fixing something). But the status message will stay until you clear it (with a timer or Fade).

Upvotes: 3

Rik Schoonbeek
Rik Schoonbeek

Reputation: 4460

I just solved my own problem.

I needed to use:

setErrors( errors )

instead of:

setErrors({ errors })

Upvotes: 3

Related Questions