17

I'm working with a react form validation using Yup along with Formik. There is a react-select element in the form which needs to be validated as well. For validation i'm making use of validationSchema of Formik to validate form on value change. I need only value of the select field as a string so cant take the complete object (key-value). The select field is working fine how ever the validation error message is not cleared. The question is how can I validate the select field with existing approach?

Below is the minimal code sample.

import ReactDOM from "react-dom";
import React, { useState } from "react";
import { Grid, TextField, Button } from "@material-ui/core";
import { Formik } from "formik";
import * as Yup from "yup";
import Select from "react-select";
import "./styles.css";

function App() {
  const [selectedYear, setSelectedYear] = useState("");

  const testSchema = Yup.object().shape({
    name: Yup.string().required("Enter Name"),
    year: Yup.string().required("Select Year")
  });

  const initialValues = {
    name: "",
    year: ""
  };

  const handleYearChange = (selectedYear, values) => {
    values.year = selectedYear.value;
    console.log(selectedYear);
    setSelectedYear(selectedYear);
  };

  const yearOptions = [
    { value: "1960", label: "1960" },
    { value: "1961", label: "1961" },
    { value: "1962", label: "1962" },
    { value: "1963", label: "1963" },
    { value: "1964", label: "1964" },
    { value: "1965", label: "1965" }
  ];

  return (
    <Formik validationSchema={testSchema} initialValues={initialValues}>
      {({
        handleChange,
        handleBlur,
        values,
        errors,
        touched,
        handleSubmit,
        setFieldTouched
      }) => {
        return (
          <>
            <Grid container spacing={2}>
              <Grid item md={12} xs={12}>
                <TextField
                  label="Name"
                  name="name"
                  margin="normal"
                  variant="outlined"
                  onChange={handleChange("name")}
                  style={{ width: "100%", zIndex: 0 }}
                  value={values.name}
                  onBlur={() => {
                    console.log("name");
                  }}
                />
                {errors.name}
              </Grid>

              <Grid item md={6} xs={12}>
                <Select
                  placeholder="Year"
                  value={selectedYear}
                  onChange={selectedOption => {
                    handleYearChange(selectedOption);
                    // handleYearChange(selectedOption, values);
                    // values.year = selectedOption.value;
                    console.log("values", values.year);
                    handleChange("year");
                  }}
                  isSearchable={true}
                  options={yearOptions}
                  name="year"
                  isLoading={false}
                  loadingMessage={() => "Fetching year"}
                  noOptionsMessage={() => "Year appears here"}
                />
                {errors.year}
              </Grid>
              <Grid
                item
                md={4}
                style={{ marginTop: "24px", marginBottom: "10px" }}
                xs={12}
              >
                <Button onClick={handleSubmit}>Save</Button>
              </Grid>
            </Grid>
          </>
        );
      }}
    </Formik>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Here is the codesandbox :

Edit throbbing-shadow-6f6yw

PS: I'm new to Reactjs.

Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
Nik
  • 1,589
  • 2
  • 15
  • 23

6 Answers6

15

Change

handleChange("year")

To

handleChange("year")(selectedOption.value);

Currently the year field in the Formik value isn't updated. The handleChange() function returns a new function that can be called with a value to update the Formik state.

Easiest way to spot these things is by outputting the Formik props with the following code:

<pre>{JSON.stringify(props, null, 2)}</pre>

See this sandbox for an example. In the sandbox I have completely removed the need for the custom year state. I'd recommend using only the Formik state to manipulate the values. Using only Formik state you will probably have to extract only the year part when saving, because react-select uses the complete object by default.

Jap Mul
  • 17,398
  • 5
  • 55
  • 66
  • That did the trick, however if i use `handleChange("year")(selectedOption.value);` it does not show the selected value on `react-select` box but reflects in `values` object. However if i do `handleChange("year")(selectedOption);` then it reflects the selected option on `react-select` and the values object seems to have nested object. Is there any way where the `values` object has only string value and not the nested object? – Nik Aug 22 '19 at 09:12
  • 2
    The `react-select` package requires the complete object by default. The writer wrote a separate package to enable passing only the value part, check it out: https://github.com/jossmac/react-select-simple-value – Jap Mul Aug 22 '19 at 09:16
  • @jagsler, thank you very much for the desicion because it helped me to resolve the same issue. – Aleksei Korkoza Oct 23 '19 at 07:13
  • i don't know why but this method throw my an error saying Cannot read property 'persist' of undefined – Jorge Tejeda Oct 03 '20 at 15:10
11

You need to fool Formik into considering this as an event. handleChange here is from Formik. This also works for any other input field type such as react-color, or datepicker.

validationSchema = yup.object({
  year_value: yup.object().required('Year value is required.')
})
<Select 
  className=""
  name="year_value"
  id="year_value"
  placeholder='Choose year value'
  value={values.year_value}
  onBlur={handleBlur}
  onChange={selectedOption => {
    let event = {target: {name: 'year_value', value: selectedOption}}
    handleChange(event)
  }}
  onBlur={() => {
    handleBlur({target: {name: 'year_value'}});
  }}
  options={yearOptions}
/>

simmer
  • 2,639
  • 1
  • 18
  • 22
Manthan Patel
  • 260
  • 4
  • 9
3

In case somebody is looking for solutions to this in the future, here's an alternative approach to get the selected value as a string from react-select -- and still have Yup perform the validation on the select input:

Using the helpers function from useField(), you can set the value, touched, and error state of a "Field". useField() is helpful any time you're working with elements that aren't inputs, like react-select.

function FormikSelect(...props) {
  const [field, meta, helpers] = useField(name="mySelectInput"); // can pass 'props' into useField also, if 'props' contains a name attribute
  const { setValue, setTouched, setError } = helpers;

  const setFieldProps = (selectedOption) => {
      setValue(selectedOption.value) 
      setTouched(true)
      setError(undefined)
  }

  return (
        <Select onChange={selectedOption => setFieldProps(selectedOption)} />
  );
};
kablamus
  • 921
  • 2
  • 10
  • 17
  • One feedback I have would be to not to group these in the `onChange` and to at least break out the `setTouched` method call and attach it to the `onBlur` prop as that's how touched events are generally triggered. – Sgnl Mar 12 '21 at 20:23
2

I solve my problem by using following code.

import this at the top

import Select from 'react-select';
import { Formik, Form, Field } from 'formik';

now write this code at jsx render part

<Formik
      initialValues={initialUserAddData}
      validationSchema={addUserValidationSchema}
      onSubmit={handleAddUser}
    >
      {({ errors }) => (
        <Form className="add-edit-user-form">
          <Field name="department">
            {({ field, form }) => (
               <Select
                 className="select-wrap"
                 classNamePrefix="select-box"
                 options={department}
                 onChange={(selectedOption) =>
                     form.setFieldValue(
                       'department',
                       selectedOption.value,
                      )
                    }
               />
            )}
         </Field>
         {errors.department && (
           <span className="error">{errors.department}</span>
         )}
       </Form>
      )}
 </Formik>

its only the example for use react-select with formik and update the value in formik validation you can also use useFormik hook for the same but this is the different way

Krishna Jangid
  • 4,961
  • 5
  • 27
  • 33
1

I would use Formik select instead of react select like this:

    const initialValues = {
        name: "",
        year: ""
    };

    const testSchema = Yup.object().shape({
      name: Yup.string().required("Enter Name"),
      year: Yup.string().required("Select Year")
    });

    <Field as="select" name="year" id="year">
    <option value="" label="">
    Select Your Year{" "}
    </option>
    {yearOptions.map(item => 
       <option value={item.value} label={item.label}>{item.value}</option>
   )} 
    </Field>

use map on options array to make options

0

In case someone is still looking for an asnwer. This will help.

Initially create a custom component using Select component.

import Select from "react-select";

export default function CustomSelect({ onChange, options, value, name, className = "" }) {
    const defaultValue = (options, value) => {
        return options ? options.find((option) => option.value === value) : "";
    };

    return (
        <div>
            <Select
                className={className}
                name={name}
                value={defaultValue(options, value)}
                onChange={(value) => {
                    onChange(value);
                }}
                options={options}
            />
        </div>
    );
}

Import the custom component and use as follows. Make sure to use setFieldTouched and setFieldValue from formik props, to set and validate the form.

<CustomSelect
   name="name"
   value={"value"}
   options={options}
   onChange={(option) => {
   setFieldTouched("name", true);
   setFieldValue("name", option.value);
   }}
/>