How to update select items based on the selection from the first select items using react

Viewed 7996

I have an array of objects like this:

 const items = [ 
  { country:"USA", level:1},
  { country:"Canada", level:2},
  { country:"Bangladesh", level:3},
]

And in my Form component( I am using React Formik library)I am rendering the items like this:

     <Field name="color" as="select" placeholder="Favorite Color">
         {items.map((item,index)=>(
           <option>{item.country}</option>
         ))}
    </Field>
    <Field name="color" as="select" placeholder="Favorite Color">
         {items.map((item,index)=>(
           <option>{item.level}</option>
         ))}
    </Field>

Now,I need to update the value of second select items based on the items selection from the first select input. For example, when I will select "USA" from the first selection then in the second selection it will update the value and render "1". Any ideas or suggestion would be highly appreciated. So far, my component looks like this:

import React from 'react';
import { Formik, Field } from 'formik';
import { Modal } from 'react-bootstrap';

const AddNewForm = () => {
const items = [ 
  { country:"USA", level:1},
  { country:"Canada", level:2},
  { country:"Bangladesh", level:3},
]
const handleUpdate = () => {
  console.log("change value...")
}
return (
    <div>
  <Formik
  initialValues={{ country: '', level: '' }}
  onSubmit={(values, { setSubmitting }) => {
    setTimeout(() => {
      alert(JSON.stringify(values, null, 2));
      setSubmitting(false);
    }, 400);
  }}
>
  {({
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    isSubmitting,
    /* and other goodies */
  }) => (
    <form onSubmit={handleSubmit}>
      <Field name="country" as="select" onChange={handleUpdate}>
         {items.map((item,index)=>(
           <option>{item.country}</option>
         ))}
    </Field>
    <Field name="level" as="select">
         {items.map((item,index)=>(
           <option>{item.level}</option>
         ))}
    </Field>
    <Modal.Footer>
    <button type="submit" disabled={isSubmitting}>
        Save
      </button>
      </Modal.Footer>
    </form>
  )}
</Formik>
    </div>
)

}export default AddNewForm;

2 Answers

As I wrote earlier ... there is no need to manage states manually, Formik manages it for us.

Version with functional conponents/hooks - useFormikContext (and <Formi /> as parent/context provider) is needed when <Field/> is used. You can use useFormik (and no parent) with normal html inputs.

Outer component:

export default function App() {
  const items = [
    { country: "USA", level: 1 },
    { country: "USA", level: 5 },
    { country: "Canada", level: 2 },
    { country: "Canada", level: 4 },
    { country: "Bangladesh", level: 2 },
    { country: "Bangladesh", level: 7 }
  ];

  return (
    <div className="App">
      <h1>connected selects</h1>
      <Formik
        initialValues={{ country: "", level: "" }}
        onSubmit={values => {
          console.log("SUBMIT: ", values);
        }}
      >
        <Form data={items} />
      </Formik>
    </div>
  );
}

Outer component (parent <Formik />) responsibility:

  • initialization
  • main handlers (submit)
  • validation

Inner component responsibility:

  • data passed as prop;
  • local data filtering (avoiding unnecessary recalculations with hooks);
  • local handlers for field dependencies;
  • visual conditional changes

Inner component:

import React, { useState, useEffect } from "react";
import { Field, useFormikContext } from "formik";
import { Modal } from "react-bootstrap";

const AddNewForm = props => {
  const items = props.data;

  // derived data, calculated once, no updates
  // assuming constant props - for changing useEffect, like in levelOptions
  const [countryOptions] = useState(
    Array.from(new Set(items.map(item => item.country)))
  );
  const [levelOptions, setLevelOptions] = useState([]);

  const {
    values,
    handleChange,
    setFieldValue,
    handleSubmit,
    isSubmitting,
    isValid // will work with validation schema or validate fn defined
  } = useFormikContext();

  const myHandleChange = e => {
    const selectedCountry = e.target.value;

    debugger;
    // explore _useFormikContext properties
    // or FormikContext in react dev tools

    console.log("myHandle selectedCountry", selectedCountry);
    handleChange(e); // update country

    // available levels for selected country
    const levels = items.filter(item => item.country === selectedCountry);
    if (levels.length > 0) {
      // update level to first value
      setFieldValue("level", levels[0].level);
      console.log("myHandle level", levels[0].level);
    }
  };

  // current values from Formik
  const { country, level } = values;

  //  calculated ususally on every render
  //
  // const countryOptions = Array.from(new Set(items.map(item => item.country)));
  // const levelOptions = items.filter(item => item.country === country);
  //
  //  converted into hooks (useState and useEffect)
  //
  useEffect(() => {
    // filtered array of objects, can be array of numbers
    setLevelOptions(items.filter(item => item.country === country));
  }, [items, country]); // recalculated on country [or items] change

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <Field
          name="country"
          value={country}
          as="select"
          onChange={myHandleChange} // customized handler
        >
          {!country && (
            <option key="empty" value="">
              Select Country - disappearing empty option
            </option>
          )}
          {countryOptions.map((country, index) => (
            <option key={country} value={country}>
              {country}
            </option>
          ))}
        </Field>
        {country && (
          <>
            <Field
              name="level"
              as="select"
              onChange={handleChange} // original handler
            >
              {levelOptions.map((item, index) => (
                <option key={item.level} value={item.level}>
                  {item.level}
                </option>
              ))}
            </Field>

            <Modal.Footer>
              <button type="submit" disabled={!isValid && isSubmitting}>
                Save
              </button>
            </Modal.Footer>
          </>
        )}
      </form>
      <>
        <h1>values</h1>
        {country && country}
        <br />
        {level && level}
      </>
    </div>
  );
};

export default AddNewForm;

Working demo, explorable/debbugable iframe here

Does it fulfill all functional requirements ?

Try this.

import React, {useState} from "react";
import { Formik, Field } from "formik";
import { Modal } from "react-bootstrap";

const AddNewForm = () => {

  const items = [
    { country: "USA", level: 1 },
    { country: "Canada", level: 2 },
    { country: "Bangladesh", level: 3 },
  ];

  return (
    <div>
      <Formik
        initialValues={{ country: "", level: "" }}
        onSubmit={(values, { setSubmitting }) => {
          setTimeout(() => {
            alert(JSON.stringify(values, null, 2));
            setSubmitting(false);
          }, 400);
        }}
      >
        {({
          values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting
          /* and other goodies */
        }) => (
          <form onSubmit={handleSubmit}>
            <Field name="country" as="select" onChange={handleChange} >
              <option value='' defaultValue>Select Country</option>
              {items.map((item, index) => (
                <>
                <option value={item.country} >{item.country}</option>
                </>
              ))}
            </Field>
            <Field name="level" as="select" onChange={handleChange} >
            <option value='' defaultValue>Select Level</option>
              {items.filter((item)=>item.country===values.country).map((item, index) => (
                <option>{item.level}</option>
              ))}
            </Field>
            <Modal.Footer>
              <button type="submit" disabled={isSubmitting}>
                Save
              </button>
            </Modal.Footer>
          </form>
        )}
      </Formik>
    </div>
  );
};

export default AddNewForm;

Try this working demo

Related