I've built a recipe app with React and Firebase, and I'm encountering a bug where I'm unable to edit a selected recipe more than once in a row. I can edit it once, but if I open the edit form again and try to submit it, it doesn't go through.
If I select a different recipe (even without reopening the form) and then return to the original one, I'm able to submit an edit again without issue.
When the submission fails, I see the following warning in the console:
Warning: An unhandled error was caught from submitForm() TypeError: Cannot read properties of undefined (reading 'indexOf')
at Function.value (index.esm2017.js:1032:19)
at Qm (index.esm2017.js:16035:32)
at firestore.js:80:17
at c (regeneratorRuntime.js:72:17)
at Generator._invoke (regeneratorRuntime.js:55:24)
at Generator.next (regeneratorRuntime.js:97:21)
at Ve (asyncToGenerator.js:3:20)
at o (asyncToGenerator.js:22:9)
at asyncToGenerator.js:27:7
at new Promise (<anonymous>)
Here's the GitHub repo, and the live site. Use the "demo" button to log in anonymously.
I apologize if this is too broad. I toyed with trying to recreate the issue on a smaller scale in CodeSandbox, but to do so, I think I'd have to rebuild a large portion of the app.
Because the issue resolves upon selecting a different recipe, I suspect it has to do with the selectedRecipe value in the app's state. However, I'm not sure exactly where the "indexOf" in the console warning is coming from.
The recipe form component:
import { useContext, useState, useEffect } from "react";
import { recipeContext } from "../../context/recipeContext";
import { Formik, Field, Form } from "formik";
import { modes } from "../modals/modalModes";
import { addRecipeToDb, updateRecipeInDb } from "../../firebase/firestore";
import * as Yup from "yup";
function RecipeForm({ modalMode, toggleModal, user }) {
const { selectedRecipe, setSelectedRecipe } = useContext(recipeContext);
const [formValues, setFormValues] = useState(null);
// Array => string separated by newlines
const convertFromArray = (array) => {
let string = "";
array.forEach((str, index) => {
if (index < 1) {
string = string + str;
} else string = string + "\n" + str;
});
return string;
};
// String separated by newlines => array
const convertToArray = (string) => {
return string.split("\n");
};
const handleSubmit = async (values) => {
const recipe = {
title: values.title,
description: values.description,
ingredients: convertToArray(values.ingredients),
directions: convertToArray(values.directions),
uid: user.uid,
};
if (modalMode === modes.create) {
recipe.labels = [];
recipe.createdAt = new Date();
await addRecipeToDb(recipe);
setSelectedRecipe(recipe);
console.log(recipe);
} else if (modalMode === modes.edit) {
recipe.labels = selectedRecipe.labels;
recipe.createdAt = selectedRecipe.createdAt;
await updateRecipeInDb(recipe, selectedRecipe.id);
setSelectedRecipe(recipe);
}
toggleModal();
};
const initialValues = {
title: "",
description: "",
ingredients: "",
directions: "",
};
useEffect(() => {
if (selectedRecipe && modalMode === modes.edit) {
setFormValues({
title: selectedRecipe.title,
description: selectedRecipe.description,
ingredients: convertFromArray(selectedRecipe.ingredients),
directions: convertFromArray(selectedRecipe.directions),
});
}
}, [modalMode, selectedRecipe]);
const validationSchema = Yup.object({
title: Yup.string()
.max(80, "Title must be less than 80 characters")
.required("Required"),
description: Yup.string().max(
400,
"Description must be less than 400 characters"
),
ingredients: Yup.string()
.max(
10000,
"That's a lot of ingredients! The limit is 10,000 characters."
)
.required("Required"),
directions: Yup.string()
.max(
10000,
"This recipe is too complicated! The limit is 10,000 characters."
)
.required("Required"),
});
return (
<Formik
initialValues={formValues || initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
enableReinitialize>
{({ errors, touched }) => (
<Form id="recipe-form" className="form recipe-form">
<div className="field-wrapper">
<label htmlFor="title">Title</label>
<div className="error">{touched.title ? errors.title : null}</div>
<Field id="title" name="title" placeholder="Cake" as="input" />
</div>
<div className="field-wrapper">
<label htmlFor="description">Description</label>
<div className="error">
{touched.description ? errors.description : null}
</div>
<Field
id="description"
name="description"
placeholder="A real cake recipe"
as="textarea"
/>
</div>
<div className="field-wrapper">
<label htmlFor="ingredients">Ingredients</label>
<p className="error">
{touched.ingredients ? errors.ingredients : null}
</p>
<Field
id="ingredients"
name="ingredients"
placeholder="milk
eggs
flour
sugar"
as="textarea"
/>
<p className="message">Type each ingredient on a new line</p>
</div>
<div className="field-wrapper">
<label htmlFor="directions">Directions</label>
<p className="error">
{touched.directions ? errors.directions : null}
</p>
<Field
id="directions"
name="directions"
placeholder="Mix everything together
Bake at 350 degrees for 30 minutes
Let cool
Serve"
as="textarea"
/>
<p className="message">Type each step on a new line</p>
</div>
</Form>
)}
</Formik>
);
}
The function that updates the recipe in Firestore:
const updateRecipeInDb = async (recipeObj, id) => {
const docRef = doc(db, "recipes", id);
try {
updateDoc(docRef, recipeObj);
} catch (e) {
console.error("Error updating document: ", e.message);
}
};
I've messed around with several things over the past few days. Adding selectedRecipe to the form component's useEffect dependency list didn't make a difference, nor did removing the validation schema. I just can't figure out exactly what's going wrong, or where the problem is occurring. Has anyone encountered something similar, or is there something obvious I'm missing?
I want to know if the way I implemented the transfer of data between two components, is the efficient way. To be more precise, in one component that is called TableComponent.js I am fetching data from an API and then with Link from react router, I am sending that data to another component called User.js.
User.js component is used for editing or creating new users based on the fact if an url has an ID passed with Link. If the Link is not sending an ID then the path will be: /users/new, but if it has an ID then the path will be: /users/ID (id of the clicked user in the table)
Let me show you the code.
I am going to show you just a part where I am sending data with Link in TableComponent.js, as other part is not necessary and the code is quite big:
<Link
to={{
pathname: `/users/${apiData.id}`,
user: {
name: apiData.name,
email: apiData.email,
status: apiData.status,
gender: apiData.gender,
},
}}
>
{apiData.name}
</Link>
This is now the User.js component:
const User = (props) => {
const { name, email, status, gender } = props.location.user;
const { id } = useParams();
const [newName, setNewName] = useState(name);
const [newEmail, setNewEmail] = useState(email);
const [newStatus, setNewStatus] = useState(status);
const [newGender, setNewGender] = useState(gender);
const updateName = (e) => {
setNewName(e.target.value);
};
const updateEmail = (e) => {
setNewEmail(e.target.value);
};
const updateStatus = (e) => {
setNewStatus(e.target.value);
};
const updateGender = (e) => {
setNewGender(e.target.value);
};
const updateData = () => {
if (id) {
axios
.put(
`${process.env.REACT_APP_API_URL}users/${id}`,
{
name: newName,
email: newEmail,
status: newStatus,
gender: newGender,
},
{
headers: {
Authorization: `Bearer ${process.env.REACT_APP_API_TOKEN}`,
},
}
)
.catch(function (error) {
if (error.response.status === 422) {
alert(
"Error: The format of email is wrong! Please put it like this: yourname#yourname.com"
);
}
});
} else {
axios
.post(
`${process.env.REACT_APP_API_URL}users/`,
{
name: newName,
email: newEmail,
status: newStatus,
gender: newGender,
},
{
headers: {
Authorization: `Bearer ${process.env.REACT_APP_API_TOKEN}`,
},
}
)
.catch(function (error) {
if (error.response.status === 422) {
alert("Error: Name or email already taken!");
}
});
}
};
return (
<Layout title={id ? "Edit User" : "Create User"} hasSearchBox={false}>
<ActionsInputsContainer data-testid="actionspage-inputs">
<NameInputContainer>
<ActionsInputs
labelName="Name:"
placeholder="Insert the name..."
value={newName}
onChange={updateName}
/>
</NameInputContainer>
<EmailInputContainer>
<ActionsInputs
labelName="Email:"
placeholder="Insert the email..."
value={newEmail}
onChange={updateEmail}
/>
</EmailInputContainer>
<SelectInput value={newStatus} onChange={updateStatus}>
<MenuItem value="active">Active</MenuItem>
<MenuItem value="inactive">Inactive</MenuItem>
</SelectInput>
<SelectInput value={newGender} onChange={updateGender}>
<MenuItem value="male">Male</MenuItem>
<MenuItem value="female">Female</MenuItem>
</SelectInput>
<ButtonsContainer data-testid="actionspage-buttons">
<Link to="/users">
<CancelButton variant="outlined" text="CANCEL"></CancelButton>
</Link>
<Link to="/users">
<ActionsButton
onClick={updateData}
variant="filled"
text="ACCEPT"
data-testid="actionspage-updatebutton"
></ActionsButton>
</Link>
</ButtonsContainer>
</ActionsInputsContainer>
</Layout>
);
};
As you can see in this component I am sending the ID like a parameter whereas other data is being send using props. Is this a good way, because now I have certain bugs for example, if I refresh the page it fails, because there are no props being send. Also is it a good practice to send certain data as url parameter and other via props. If you could help me to make this code more efficient, I would be thankful.
I'm trying to save datetime values in TodoForm but those values are not reflected in database. This is my todo model.
Todo.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const todoSchema = new Schema({
name: String,
description: String,
isDone: Boolean,
createdAt: Date,
updatedAt: Date
},
{timestamps: {createdAt: 'created_at' updatedAt: 'updated_at'}}
);
mongoose.model('todos', todoSchema);
Then Follows my todosController post route.
TodosController:
app.post('/api/todos', async (req, res) => {
const { name, description, isDone, createdAt, updatedAt} = req.body;
const todo = new Todo({
name,
description,
isDone,
createdAt,
updatedAt
});
try {
let newTodo = await todo.save();
res.status(201).send(newTodo);
} catch (err) {
if (err.name === 'MongoError') {
res.status(409).send(err.message);
}
res.status(500).send(err);
}
});
That is from backend side. Code is next is from front end side.
TodoForm.jsx
componentWillReceiveProps = nextProps => {
// Load Contact Asynchronously
const { todo } = nextProps;
if (todo._id !== this.props.todo._id) {
// Initialize form only once
this.props.initialize(todo);
this.isUpdating = true;
}
};
render() {
const { handleSubmit, loading } = this.props;
if (loading) {
return <span>Loading...</span>;
}
return (
<form onSubmit={handleSubmit}>
<Field
name='createdAt'
component={DateTimePickerInput}
dateFormat='dd-MM-yyyy'
// dateFormat='dd-MM-yyyy H:mm'
// showTimeSelect
timeFormat='HH:mm'
placeholder=' todo createdAt...'
label='CreatedAt'
/>
<Field
name='updatedAt'
component={DateTimePickerInput}
dateFormat='dd-MM-yyyy'
// dateFormat='dd-MM-yyyy H:mm'
// showTimeSelect
timeFormat='HH:mm'
placeholder='todo UpdatedAt...'
label='UpdatedAt'
/>
<Link className='btn btn-light mr-2' to='/todos'>
Cancel
</Link>
<button className='btn btn-primary mr-2' type='submit'>
{this.isUpdating ? 'Updating' : 'Create'}
</button>
</form>
);
}
}
I expect the output e.g when I complete todo form and click on submit button is "2020-09-18N14:00:30", reflected datetime value on database, but real output are empty createdAt and updatedAt datetime values.
What is wrong?
I'm calling my api from NewTodo.jsx
state = {
redirect: false
};
componentDidMount() {
this.props.newTodo();
}
submit = todo => {
return this.props
.saveTodo(todo)
.then(response => this.setState({ redirect: true }))
.catch(err => {
throw new SubmissionError(this.props.errors);
});
};
Step 1 : convert your date to string in your schema
schema
createdAt: String,
updatedAt: String
you can direclly store your data as you passed in your post request, if you want to convert it to required format then
Step 2 : use moment if you want to format your date in required format then install moment
npm install moment
const { name, description, isDone, createdAt, updatedAt} = req.body;
const todo = new Todo({
name,
description,
isDone,
createdAt : moment(createdAt).format('DD-MM-YYYY HH.mm') , //change it as per your requirement
updatedAt : moment(updatedAt).format('DD-MM-YYYY HH.mm') ,
});
I am trying to use an options array in my react app, that uses react-select for the form and where the options are stored in a firebase collection.
This all works fine when I define a const in the form with an array of options that I define with key value pairs, but I'm struggling to figure out how to replace that array with the collection stored in Firebase (Cloud Firestore).
In my form, I currently have:
const options = [
{ value: "neurosciences", label: "Neurosciences - ABS 1109" },
{ value: "oncologyCarcinogenesis", label: "Oncology and Carcinogenesis - ABS 1112" },
{ value: "opticalPhysics", label: "Optical Physics - ABS 0205" },
{ value: "fisheriesSciences", label: "Fisheries Sciences - ABS 0704" },
{ value: "genetics", label: "Genetics - ABS 0604" },
{ value: "urbanRegionalPlanning", label: "Urban and Regional Planning - ABS 1205" }
];
I want to replace this array, with a map over the document titles in the database collection.
The document name in my database has the key and each document has a single field called 'title'.
Thank in my form select I have:
<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}
/>
{errors.fieldOfResearch && touched.fieldOfResearch &&
<ErrorMessage
name="fieldOfResearch"
component="div"
className="invalid-feedback d-block"
/>}
</div>
I have read the firebase documents on using arrays, but I am missing something (probably obvious) that has led me down at least 20 different paths for how to do this.
I'm not sure if this is relevant, but my forms are built with Formik.
How do I replace the const options array with a map over key value pairs from the firebase database collection?
I have tried to define my options constant as:
const options = fsDB.collection("abs_for_codes")
but the page fills up with errors that I can't decipher. I have read this user guide, but don't understand the directions relating to indexes and I'm not even clear on whether they're what I need to know for this problem.
https://firebase.google.com/docs/firestore/query-data/queries
I have also tried:
const options = fsDB.collection("abs_for_codes").get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc))
}
but that's just guessing from trying to make sense of the documentation.
When I try the exact formulation shown in the firebase docs, as:
const options = fsDB.collection("abs_for_codes");
options.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
console.log(doc.id, ' => ', doc.data());
});
});
I get a full page of indecipherable error messages, as follows:
TypeError: options.reduce is not a function
Select.buildMenuOptions
node_modules/react-select/dist/react-select.esm.js:4123
4120 | };
4121 | };
4122 |
> 4123 | return options.reduce(function (acc, item, itemIndex) {
| ^ 4124 | if (item.options) {
4125 | // TODO needs a tidier implementation
4126 | if (!_this3.hasGroups) _this3.hasGroups = true;
View compiled
new Select
node_modules/react-select/dist/react-select.esm.js:3593
3590 |
3591 | var _selectValue = cleanValue(value);
3592 |
> 3593 | var _menuOptions = _this.buildMenuOptions(_props, _selectValue);
| ^ 3594 |
3595 | _this.state.menuOptions = _menuOptions;
3596 | _this.state.selectValue = _selectValue;
View compiled
constructClassInstance
node_modules/react-dom/cjs/react-dom.development.js:11787
11784 | new ctor(props, context); // eslint-disable-line no-new
11785 | }
11786 | }
> 11787 | var instance = new ctor(props, context);
| ^ 11788 | var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
11789 | adoptClassInstance(workInProgress, instance);
11790 | {
View compiled
updateClassComponent
node_modules/react-dom/cjs/react-dom.development.js:15265
15262 | } // In the initial pass we might need to construct the instance.
15263 |
15264 |
> 15265 | constructClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
| ^ 15266 | mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
15267 | shouldUpdate = true;
15268 | } else if (current$$1 === null) {
View compiled
beginWork
node_modules/react-dom/cjs/react-dom.development.js:16265
16262 |
16263 | var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);
16264 |
> 16265 | return updateClassComponent(current$$1, workInProgress, _Component2, _resolvedProps, renderExpirationTime);
| ^ 16266 | }
16267 |
16268 | case HostRoot:
View compiled
performUnitOfWork
node_modules/react-dom/cjs/react-dom.development.js:20285
20282 | startProfilerTimer(workInProgress);
20283 | }
20284 |
> 20285 | next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
| ^ 20286 | workInProgress.memoizedProps = workInProgress.pendingProps;
20287 |
20288 | if (workInProgress.mode & ProfileMode) {
View compiled
workLoop
node_modules/react-dom/cjs/react-dom.development.js:20326
20323 | if (!isYieldy) {
20324 | // Flush work without yielding
20325 | while (nextUnitOfWork !== null) {
> 20326 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
| ^ 20327 | }
20328 | } else {
20329 | // Flush asynchronous work until there's a higher priority event
View compiled
HTMLUnknownElement.callCallback
node_modules/react-dom/cjs/react-dom.development.js:147
144 | window.event = windowEvent;
145 | }
146 |
> 147 | func.apply(context, funcArgs);
| ^ 148 | didError = false;
149 | } // Create a global error event handler. We use this to capture the value
150 | // that was thrown. It's possible that this error handler will fire more
View compiled
invokeGuardedCallbackDev
node_modules/react-dom/cjs/react-dom.development.js:196
193 | // errors, it will trigger our global error handler.
194 |
195 | evt.initEvent(evtType, false, false);
> 196 | fakeNode.dispatchEvent(evt);
| ^ 197 |
198 | if (windowEventDescriptor) {
199 | Object.defineProperty(window, 'event', windowEventDescriptor);
View compiled
invokeGuardedCallback
node_modules/react-dom/cjs/react-dom.development.js:250
247 | function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
248 | hasError = false;
249 | caughtError = null;
> 250 | invokeGuardedCallbackImpl$1.apply(reporter, arguments);
| ^ 251 | }
252 | /**
253 | * Same as invokeGuardedCallback, but instead of returning an error, it stores
View compiled
replayUnitOfWork
node_modules/react-dom/cjs/react-dom.development.js:19509
19506 |
19507 | isReplayingFailedUnitOfWork = true;
19508 | originalReplayError = thrownValue;
> 19509 | invokeGuardedCallback(null, workLoop, null, isYieldy);
| ^ 19510 | isReplayingFailedUnitOfWork = false;
19511 | originalReplayError = null;
19512 |
View compiled
renderRoot
node_modules/react-dom/cjs/react-dom.development.js:20439
20436 | if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
20437 | if (mayReplay) {
20438 | var failedUnitOfWork = nextUnitOfWork;
> 20439 | replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
| ^ 20440 | }
20441 | } // TODO: we already know this isn't true in some cases.
20442 | // At least this shows a nicer error message until we figure out the cause.
View compiled
performWorkOnRoot
node_modules/react-dom/cjs/react-dom.development.js:21363
21360 | cancelTimeout(timeoutHandle);
21361 | }
21362 |
> 21363 | renderRoot(root, isYieldy);
| ^ 21364 | finishedWork = root.finishedWork;
21365 |
21366 | if (finishedWork !== null) {
View compiled
Another attempt:
const options = abs_for_codes.map((title) => {
<option key={title}
value={id} />
}
This doesn't work either - I tried it because it looks similar to the react arrays instructions.
The attached image shows the data structure in firestore.
NEXT ATTEMPT
Using Murray's suggestion, I have tried
import Select from "react-select";
import { fsDB, firebase, settings } from "../../../firebase";
let options = [];
const initialValues = {
fieldOfResearch: null,
}
class ProjectForm extends React.Component {
state = {
selectedValue1: options,
}
handleSelectChange1 = selectedValue1 => {
this.setState({ selectedValue1 });
};
componentDidMount() {
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
});
});
this.setState({options: newOptions});
});
}
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 });
this.setState({ selectedValue2: null });
this.setState({ selectedValue3: null });
this.setState({ selectedValue4: null });
this.setState({ selectedValue5: null });
this.setState({ selectedValue6: null });
resetForm(initialValues);
})
.catch(error => {
console.error("Error adding document: ", error);
});
};
onSubmit={this.handleSubmit}
render={({ errors, status, touched, setFieldTouched, handleSubmit, values }) => {
let fieldOfResearch;
const handleChange1 = optionsObject => {
fieldOfResearch = optionsObject;
return (values.fieldOfResearch = optionsObject.value);
};
<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}
/>
{errors.fieldOfResearch && touched.fieldOfResearch &&
<ErrorMessage
name="fieldOfResearch"
component="div"
className="invalid-feedback d-block"
/>}
</div>
So, stepping that through, options starts as an empty array, the ComponentDidMount function resets its state to NewOptions and that gets fed into the form select drop down.
That all makes sense to me, but it doesn't work - I just get an empty array.
When I try Avanthika's suggestion, i can render the form and multiple options can be selected from the right db collection, but nothing happens when I submit the form. The console debugger in react shows an unsmiling face (I've never seen that before. Pic below). This form submits fine when I remove the select field.
next attempt
when i try each of Murray R and Avinthika's updated suggestions below I can choose multiple fields. BUT i cannot submit the form. The form submits if i remove the select field. Is there a trick to submitting formik multi field forms?
My submit button is:
<div className="form-group">
<Button
variant="outline-primary"
type="submit"
style={style3}
id="ProjectId"
onClick={handleSubmit}
disabled={!dirty || isSubmitting}
>
Save
</Button>
</div>
My handle submit has:
handleSubmit = (formState, { resetForm }) => {
// Now, you're getting form state here!
console.log("SUCCESS!! :-)\n\n", formState);
fsDB
.collection("project")
.add({
...(formState),
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(docRef => {
console.log("docRef>>>", docRef);
this.setState({ selectedValue1: null, selectedValue2: null, selectedValue3: null, selectedValue4: null, selectedValue5: null, selectedValue6: null });
// this.setState({ selectedValue1: null });
// this.setState({ selectedValue2: null });
// this.setState({ selectedValue3: null });
// this.setState({ selectedValue4: null });
// this.setState({ selectedValue5: null });
// this.setState({ selectedValue6: null });
resetForm(initialValues);
})
.catch(error => {
console.error("Error adding document: ", error);
});
};
The console doesn't log anything.
next attempt
I removed and reinstalled the react chrome extension and that's working again.
The attached screen shot shows the form isn't validating and isn't submitting, but the state of each of the form values is in there - you can see the bottom of the shot shows one of the form field values as 's'.
further attempt
So - I split this form out into a form that only has one field- the select field that I have been trying to work on here.
That form, in its entirety, has:
import React from 'react';
import { Formik, Form, Field, ErrorMessage, withFormik } from "formik";
import * as Yup from "yup";
import Select from "react-select";
import { fsDB, firebase, settings } from "../../../firebase";
import {
Badge,
Button,
Col,
ComponentClass,
Feedback,
FormControl,
FormGroup,
FormLabel,
InputGroup,
Table,
Row,
Container
} from "react-bootstrap";
const initialValues = {
fieldOfResearch: null,
}
class ProjectForm extends React.Component {
state = {
options: [],
selectedValue1: [],
}
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 + ' - ABS ' + doc.id
});
});
});
this.setState({
options
});
}
handleSelectChange1 = selectedValue1 => {
this.setState({ selectedValue1 });
};
handleSubmit = (formState, { resetForm }) => {
// Now, you're getting form state here!
console.log("SUCCESS!! :-)\n\n", formState);
fsDB
.collection("project")
.add({
...(formState),
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(docRef => {
console.log("docRef>>>", docRef);
this.setState({ selectedValue1: null});
resetForm(initialValues);
})
.catch(error => {
console.error("Error adding document: ", error);
});
};
render() {
const { options } = this.state;
return (
<Formik
initialValues={initialValues}
validationSchema={Yup.object().shape({
// fieldOfResearch: Yup.array().required("What is your field of research?"),
})}
onSubmit={this.handleSubmit}
render={({ errors, status, touched, setFieldTouched, handleSubmit, isSubmitting, dirty, values }) => {
let fieldOfResearch;
const handleChange1 = optionsObject => {
fieldOfResearch = optionsObject;
return (values.fieldOfResearch = optionsObject.value);
};
return (
<div>
<Form>
<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}
/>
{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>
);
}}
/>
);
}
}
export default ProjectForm;
This form allows the selection of a field of research in the form. The on submit function works in the console, to the extent that it logs success with a fieldOfResearch as 'undefined'. Nothing persists to the database.
The error message says: Unhandled Rejection (FirebaseError): Function
DocumentReference.set() called with invalid data. Unsupported field
value: undefined (found in field fieldOfResearch) ▶
When I try to enter a field value and inspect the react value, the error message says:
Uncaught TypeError: Cannot convert undefined or null to object
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:
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)}
}
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}
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.
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.
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);
});
};
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.
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.