score:3

Accepted answer

Another updated answer:

The error message says: Unhandled Rejection (FirebaseError): Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field fieldOfResearch

This error happened because your form values not valid. You are not maintaining proper formik state.

I just tried this and checked, form submit works great for me. You've written too much of excess code - we just need native formik methods and firebase. The change log is as follows:

  1. The onChange of react-select should use setFieldValue from Formik like this:
onChange={selectedOptions => {
   // Setting field value - name of the field and values chosen.
   setFieldValue("fieldOfResearch", selectedOptions)}
}
  1. The initial value should be an empty array. Since we have initialValues declared and the formvalues maintained via Formik, there's absolutely no need for internal state management. I.E, there's no need for this.state.selectedValue1, handleChange1 and handleSelectChange1. If you take a look at the render() of your Formik HOC, you'll notice values - This gives current value of the form after every change.

So,

value={this.state.selectedValue1}

should be changed to

value={values.fieldOfResearch}
  1. I've written the handleSubmit like this - The exact replica of your code. But I'm only extracting values from the array of selected options:
handleSubmit = (formState, { resetForm }) => {
  // Now, you're getting form state here!
  const fdb = firebase.firestore();
  const payload = {
    ...formState,
    fieldOfResearch: formState.fieldOfResearch.map(t => t.value)
  }
  console.log("formvalues", payload);
  fdb
  .collection("project")
  .add(payload)
  .then(docRef => {
    console.log("docRef>>>", docRef);
    resetForm(initialValues);
  })
  .catch(error => {
    console.error("Error adding document: ", error);
  });
}

I'm able to see the form submission & the docRef in the console. The form also gets reset to initial state.

import React from "react";
import { Formik, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import Select from "react-select";
import firebase from "./firebase";
import {
  Button,
  Container
} from "react-bootstrap";

const initialValues = {
  fieldOfResearch: []
};

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      options: []
    };
  }

  async componentWillMount() {
    const fdb = firebase.firestore();
    let options = [];
    await fdb
      .collection("abs_codes")
      .get()
      .then(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
          options.push({
            value: doc.data().title.replace(/( )/g, ""),
            label: doc.data().title
          });
        });
      });
    this.setState({
      options
    });
  }

  handleSubmit = (formState, { resetForm }) => {
    // Now, you're getting form state here!
    const fdb = firebase.firestore();
    const payload = {
      ...formState,
      fieldOfResearch: formState.fieldOfResearch.map(t => t.value)
    }
    console.log("formvalues", payload);
    fdb
    .collection("project")
    .add(payload)
    .then(docRef => {
      console.log("docRef>>>", docRef);
      resetForm(initialValues);
    })
    .catch(error => {
      console.error("Error adding document: ", error);
    });
  }

  render() {
    const { options } = this.state;
    return (
      <Container>
        <Formik
          initialValues={initialValues}
          validationSchema={Yup.object().shape({
              fieldOfResearch: Yup.array().required("What is your field of research?"),
          })}
          onSubmit={this.handleSubmit}
          render={({
            errors,
            status,
            touched,
            setFieldValue,
            setFieldTouched,
            handleSubmit,
            isSubmitting,
            dirty,
            values
          }) => {
            return (
              <div>
                <Form>
                  <div className="form-group">
                    <label htmlFor="fieldOfResearch">
                      Select your field(s) of research
                    </label>
                    <Select
                      key={`my_unique_select_keyfieldOfResearch`}
                      name="fieldOfResearch"
                      isMulti
                      className={
                        "react-select-container" +
                        (errors.fieldOfResearch && touched.fieldOfResearch
                          ? " is-invalid"
                          : "")
                      }
                      classNamePrefix="react-select"
                      value={values.fieldOfResearch}
                      onChange={selectedOptions => {
                        setFieldValue("fieldOfResearch", selectedOptions)}
                      }
                      onBlur={setFieldTouched}
                      options={options}
                    />
                    {errors.fieldOfResearch && touched.fieldOfResearch &&
                      <ErrorMessage
                         name="fieldOfResearch"
                         component="div"
                         className="invalid-feedback d-block"
                      />
                    }
                  </div>
                  <div className="form-group">
                    <Button
                      variant="outline-primary"
                      type="submit"
                      id="ProjectId"
                      onClick={handleSubmit}
                      disabled={!dirty || isSubmitting}
                    >
                      Save
                    </Button>
                  </div>
                </Form>
              </div>
            );
          }}
        />
      </Container>
    );
  }
}

export default App;

Just try copy pasting this first, and on top of this, try making your changes. I guess this should be helpful for you!


Updated Answer:

Hi Mel, I just set the whole thing in my system and tried doing it for you, although I cannot share the creds with you, I guess this should help.

  1. Javascript is not synchronous. Your componentDidMount will not wait for the data you're trying to get from firebase. It will just set the state before your query returns response.

  2. They key is to await the response. I've edited the code that way, and I'm able to see the options on my console in the render().

import React from 'react';
import firebase from "./firebase.js";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      options: []
    }
  }

  async componentDidMount() {
    const fsDB = firebase.firestore(); // Don't worry about this line if it comes from your config.
    let options = [];
    await fsDB.collection("abs_for_codes").get().then(function (querySnapshot) {
    querySnapshot.forEach(function(doc) {
        console.log(doc.id, ' => ', doc.data());
        options.push({
          value: doc.data().title.replace(/( )/g, ''),
          label: doc.data().title
        });
      });
    });
    this.setState({
      options
    });
  }

  render() {
    console.log(this.state);
    const { options } = this.state;
    return (
      <div className="form-group">
        <label htmlFor="fieldOfResearch">
          Select your field(s) of research
        </label>

        <Select
          key={`my_unique_select_key__${fieldOfResearch}`}
          name="fieldOfResearch"
          isMulti
          className={
            "react-select-container" +
            (errors.fieldOfResearch && touched.fieldOfResearch
              ? " is-invalid"
              : "")
          }
          classNamePrefix="react-select"
          value={this.state.selectedValue1}
          onChange={e => {
            handleChange1(e);
            this.handleSelectChange1(e);
          }}
          onBlur={setFieldTouched}
          options={options}
        />
      </div>
    );
  }
}

export default App;

Let me know if this works for you!

And I couldn't help but notice, why so many setStates in handleSubmit? You're forcing your component to rerender that many times. Instead you can do:

handleSubmit = (formState, { resetForm }) => {
    // Now, you're getting form state here!
    console.log("SUCCESS!! :-)\n\n", formState);
    fsDB
      .collection("project")
      .add(formState)
      .then(docRef => {
        console.log("docRef>>>", docRef);
        this.setState({ selectedValue1: null, selectedValue2: null, selectedValue3: null, selectedValue4: null, selectedValue5: null, selectedValue6: null });
        resetForm(initialValues);
      })
      .catch(error => {
        console.error("Error adding document: ", error);
      });
  };

score:1

So would something like this help?

function SomeComponentName(props) {
  const [options, setOptions] = React.useState([]);

  React.useEffect(() => {
    getOptions()
  }, []}

  async function getOptions() {
    const tmpArr = [];

    try {
      // Perform get() request and loop through all docs
      await fsDB
        .collection("abs_codes")
        .get()
        .then(snapshot => {
          snapshot.forEach(doc => {
            const { title } = doc.data();
            const label = `${title} - ABS ${doc.key}`;

            tmpArr.push({ value: title, label });
          });
          setOptions(tmpArr);
        });
    } catch (err) {
      console.log("Error getting documents", err);
    }
  }

  return (
    <div className="form-group">
      <label>
        <Select
          // ...
          options={options}
          // ...
        />
      </label>
    </div>
  );
}

This will get all documents within the 'abs_code' collection, loop through them, and push each entry as an object to the 'options' array.

score:1

For your React component to update after retrieving your research fields from Firestore, you'll need to declare your options in a way that React will pay attention to its value changes. You can do this by saving options within the class's state and update it later using setState():

// At the top of the class,
state = {
  options: [],
  // Any other state properties,
};

Then, within the componentDidMount() function, make your call to your Firestore collection and populate state.options with the results:

fsDB.collection("abs_for_codes").get().then(function (querySnapshot) {
    let newOptions = [];
    querySnapshot.forEach(function (doc) {
        console.log(doc.id, ' => ', doc.data());
        newOptions.push({
          value: doc.data().title.replace(/( )/g, ''),
          label: doc.data().title + ' - ABS ' + doc.id
        });
    });
    setState({options: newOptions});
});

This should retrieve your 'abs_for_codes' documents from Firestore and assign them to your component's options property in a way that will allow your Select element to be populated with the new data.


Related Query

More Query from same tag