I am trying to delete multiple items on click of checkbox using firestore. But, onSnapshot method of firestore is causing issue with the state.
After running the code I can click on checkbox and delete the items, the items get deleted too but I get an error page, "TyperError: this.setState is not a function" in onCollectionUpdate method.
After refreshing the page I can see the items deleted.
Here's my code:
class App extends React.Component {
constructor(props) {
super(props);
this.ref = firebase.firestore().collection('laptops');
this.unsubscribe = null;
this.state = { laptops: [], checkedBoxes: [] };
this.toggleCheckbox = this.toggleCheckbox.bind(this);
this.deleteProducts = this.deleteProducts.bind(this);
}
toggleCheckbox = (e, laptop) => {
if (e.target.checked) {
let arr = this.state.checkedBoxes;
arr.push(laptop.key);
this.setState = { checkedBoxes: arr };
} else {
let items = this.state.checkedBoxes.splice(this.state.checkedBoxes.indexOf(laptop.key), 1);
this.setState = {
checkedBoxes: items
}
}
}
deleteProducts = () => {
const ids = this.state.checkedBoxes;
ids.forEach((id) => {
const delRef = firebase.firestore().collection('laptops').doc(id);
delRef.delete()
.then(() => { console.log("deleted a laptop") })
.catch(err => console.log("There is some error in updating!"));
})
}
onCollectionUpdate = (querySnapshot) => {
const laptops = [];
querySnapshot.forEach((doc) => {
const { name, price, specifications, image } = doc.data();
laptops.push({
key: doc.id,
name,
price,
specifications,
image
});
});
this.setState({ laptops });
console.log(laptops)
}
componentDidMount = () => {
this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate);
}
getLaptops = () => {
const foundLaptops = this.state.laptops.map((laptop) => {
return (
<div key={laptop.key}>
<Container>
<Card>
<input type="checkbox" className="selectsingle" value="{laptop.key}" checked={this.state.checkedBoxes.find((p) => p.key === laptop.key)} onChange={(e) => this.toggleCheckbox(e, laptop)} />
...carddata
</Card>
</Container>
</div>
);
});
return foundLaptops;
}
render = () => {
return (
<div>
<button type="button" onClick={this.deleteProducts}>Delete Selected Product(s)</button>
<div className="row">
{this.getLaptops()}
</div>
</div>
);
}
}
export default App;
In the toggleCheckbox function you set the this.setState to a object.
You will need to replace that with this.setState({ checkedBoxes: items})
So you use the function instead of setting it to a object
You probably just forgot to bind the onCollectionUpdate so this referes not where you expectit to refer to.
Can you pls also change the this.setState bug you have there as #David mentioned also:
toggleCheckbox = (e, laptop) => {
if (e.target.checked) {
let arr = this.state.checkedBoxes;
arr.push(laptop.key);
this.setState({ checkedBoxes: arr });
} else {
let items = this.state.checkedBoxes.splice(this.state.checkedBoxes.indexOf(laptop.key), 1);
this.setState({
checkedBoxes: items
})
}
}
If you already did that pls update your question with the latest code.
Related
I have some elements inside an array that shares a same state. I need to update only the clicked one in order to add one more item to my shopping cart. How can i do this without changing the others?
My initial state looks like this:
class ShoppingCart extends Component {
constructor() {
super();
this.state = {
isEmpty: true,
cartItems: [],
count: 0,
};
this.getStoredProducts = this.getStoredProducts.bind(this);
this.handleButtonIncrease = this.handleButtonIncrease.bind(this);
}
componentDidMount() {
this.getStoredProducts();
}
handleButtonIncrease() {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
getStoredProducts() {
const getFromStorage = JSON.parse(localStorage.getItem('cartItem'));
if (getFromStorage !== null) {
this.setState({
cartItems: getFromStorage,
}, () => {
const { cartItems } = this.state;
if (cartItems.length) {
this.setState({ isEmpty: false });
}
});
}
}
render() {
const { isEmpty, cartItems, count } = this.state;
const emptyMsg = (
<p data-testid="shopping-cart-empty-message">Seu carrinho está vazio</p>
);
return (
<div>
{ isEmpty ? (emptyMsg) : (cartItems.map((item) => (
<ShoppingCartProduct
key={ item.id }
id={ item.id }
count={ count }
cartItems={ item }
handleButtonIncrease={ this.handleButtonIncrease }
/>
)))}
</div>
);
}
}
It seems like this should be ShoppingCartProduct's responsibility. If you remove this count and setCount logic from your ShoppingCart component and create it inside of the ShoppingCartProducts component, each one of the items will have their own count state that can be updated independently.
One other way of seeing this is directly mutating each cartItem, but since you didn't specify their format there's no way of knowing if they're storing any information about quantity so I would go with the first approach.
handleButtonIncrease can accept item.id as parameter so that it can update the state.cartItems.
handleButtonIncrease(itemId) {
const cartItems = this.state.cartItems.map(item => {
return item.id === itemId
? {
// apply changes here for the item with itemId
}
: item
});
this.setState((prevState) => ({
cartItems,
count: prevState.count + 1,
}));
}
After that, update your callback as well:
handleButtonIncrease={ () => this.handleButtonIncrease(item.id) }
I am working on a react app where I have a userSettings screen for the user to update their settings on clicking a save button. I have two sliding switches that are saved and a dispatch function is ran to post the data.
Each switch has their own toggle function, and all the functions run at the same time.
My problem is that when I pass the userSettings object to the child component and run both functions, it runs with the wrong values which results in the data not saving properly.
Here is my code:
Parent component that has the toggle functions, handles the state, and set the userSettings object:
class SideMenu extends React.PureComponent {
constructor(props) {
super(props);
const userToggleSettings = {
cascadingPanels: this.props.userSettings.usesCascadingPanels,
includeAttachments: this.props.userSettings.alwaysIncludeAttachments,
analyticsOptIn: false,
};
this.state = {
userToggleSettings,
};
}
toggleIncludeAttachments = () => {
this.setState((prevState) => ({
userToggleSettings: {
...prevState.userToggleSettings,
includeAttachments: !prevState.userToggleSettings.includeAttachments,
},
}));
};
toggleCascadingPanels = () => {
this.setState((prevState) => ({
userToggleSettings: {
...prevState.userToggleSettings,
cascadingPanels: !prevState.userToggleSettings.cascadingPanels,
},
}));
};
includeAttachmentsClickHandler = () => {
this.toggleIncludeAttachments();
};
cascadingPanelsClickHandler = () => {
this.toggleCascadingPanels();
};
render() {
const darkThemeClass = this.props.isDarkTheme ? "dark-theme" : "";
const v2Class = this.state.machineCardV2Enabled ? "v2" : "";
const phAdjustmentStyle = this.getPersistentHeaderAdjustmentStyle();
const closeButton =
(this.state.machineListV2Enabled &&
this.props.view === sideMenuViews.USER_SETTINGS) ||
(!this.props.wrapper && this.props.view === sideMenuViews.SETTINGS);
return (
<div className="sideMenuFooter">
<SideMenuFooterContainer
userToggleSettings={this.state.userToggleSettings} //HERE IS USER_SETTINGS PASSED
/>
</div>
);
}
}
The child component that dispatches the data
SideMenuFooterContainer:
export function mapStateToProps(state) {
return {
translations: state.translations,
userSettings: state.appCustomizations.userSettings,
};
}
export function mapDispatchToProps(dispatch) {
return {
toggleCascadingPanels: (hasCascadingPanels) =>
dispatch(userSettingsDux.toggleCascadingPanels(hasCascadingPanels)),
toggleIncludeAttachments: (hasIncludeAttachments) =>
dispatch(userSettingsDux.toggleIncludeAttachments(hasIncludeAttachments)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SideMenuFooter);
SideMenuFooterView (where it calls the dispatch):
const saveUserSettings = (props) => {
console.log("userSettings ==>\n");
console.log(props.userToggleSettings);
props.toggleIncludeAttachments(props.userToggleSettings.includeAttachments);
props.toggleCascadingPanels(props.userToggleSettings.cascadingPanels);
};
const cancelButtonClickHandler = (props) => {
if (props.viewTitle === props.translations.USER_SETTINGS) {
return () => props.closeSideMenu();
}
return () => props.viewBackButtonCallback();
};
const doneSaveButtonsClickHandler = (props) => {
return () => {
saveUserSettings(props);
props.closeSideMenu();
};
};
const SideMenuFooter = (props) => {
return (
<div className="side-menu-footer">
<div className="side-menu-footer-container">
<button
className="btn btn-secondary"
onClick={cancelButtonClickHandler(props)}
>
{props.translations.CANCEL}
</button>
<button
className="btn btn-primary"
onClick={doneSaveButtonsClickHandler(props)}
>
{props.translations.SAVE}
</button>
</div>
</div>
);
};
export default SideMenuFooter;
Dispatch functions:
export function toggleIncludeAttachments(hasIncludeAttachments) {
return async (dispatch, getState) => {
const { translations, appCustomizations } = getState();
const updatedUserSettings = {
...appCustomizations.userSettings,
alwaysIncludeAttachments: hasIncludeAttachments,
};
try {
await saveAppCustomizationByName(
CUSTOMIZATIONS.USER_SETTINGS,
updatedUserSettings
);
dispatch(setSettings(updatedUserSettings));
} catch (err) {
dispatch(
bannerDux.alertBanne({
description: "FAILED TO UPDATE USER DATA",
})
);
}
};
}
export function toggleCascadingPanels(hasCascadingPanels) {
return async (dispatch, getState) => {
const { translations, appCustomizations } = getState();
const updatedUserSettings = {
...appCustomizations.userSettings,
usesCascadingPanels: hasCascadingPanels,
};
try {
await saveAppCustomizationByName(
CUSTOMIZATIONS.USER_SETTINGS,
updatedUserSettings
);
dispatch(setSettings(updatedUserSettings));
} catch (err) {
dispatch(
bannerDux.alertBanner({
description: "FAILED TO UPDATE USER DATA",
})
);
}
};
}
Here is a demo:
When I set them both to false and console log the values, it looks like it is getting the correct values, but in the network call, it is getting different values on different calls
console.log output:
First network call to save data header values:
Second network call to save data header values:
NOTE: The dispatch functions work correctly, they where there before all the edits. I am changing the way it saves the data automatically to the save button using the same functions defined before.
Did I miss a step while approaching this, or did I mishandle the state somehow?
This is the updated code now. Let me know if something is not correct as I am able to compile but the issue still persists
constructor(props) {
super(props);
this.polyglot = PolyglotFactory.getPolyglot(props.pageLang);
this.state = {
otherInvestorSubtype: props.otherInvestorSubtype,
};
}
shouldRenderOtherSubtype = () => this.props.otherInvestorSubtype === OTHER_INVESTOR_SUBTYPE;
shouldRenderSubtype = () => {
const { investorTypeOptions, investorType } = this.props;
const investorTypeOption = investorTypeOptions.find(({ value }) => value === investorType);
return investorTypeOption !== undefined && investorTypeOption.subtypes.length > 0;
}
handleOtherInvestorSubtypeChange = (e) => {
this.setState({
otherInvestorSubtype: e.target.value,
});
this.props.handleOtherInvestorSubtypeChange();
}
renderSelectOtherSubtype = () => {
const { handleOtherInvestorSubtypeChange,
showError, registerChildValidator, pageLang } = this.props;
const { otherInvestorSubtype } = this.state;
return (
<ValidatedText
name="investor_subtype_other"
value={otherInvestorSubtype}
onChange={this.handleOtherInvestorSubtypeChange}
showError={showError}
registerValidation={registerChildValidator}
validation={validation(this.polyglot.t('inputs.investorTypes'), pageLang, rules.required())}
required
/>
);
}
This is the only information I have got for this. Let me know if something is missing.
It seems like you've set the textbox value as otherInvestorSubtype which is provided by the props. This way, the component (so that the textbox value) is not updated on textbox value change. You need to store the otherInvestorSubtype value provided by props in the InvestorType component's state as the following, in order to update the InvestorType component every time the user types something:
constructor(props) {
...
this.state {
otherInvestorSubtype: props.otherInvestorSubtype
}
}
change the renderSelectOtherSubtype method as the following:
renderSelectOtherSubtype = () => {
const { handleOtherInvestorSubtypeChange, showError, registerChildValidator, pageLang } = this.props;
const { otherInvestorSubtype } = this.state
return (
<ValidatedText
...
value={ otherInvestorSubtype }
onChange={ this.handleOtherInvestorSubtypeChange }
...
/>
);
}
and finally handle the textbox change on this component:
handleOtherInvestorSubtypeChange = (e) => {
this.setState({
otherInvestorSubtype: e.target.value
});
this.props.handleOtherInvestorSubtypeChange();
}
Hope this helps, and sorry if I have any typos.
I want to add a list of topics into the firebase database. Each user in the database will be able to add a list of topics that are unique to them.
It adds the data correctly but when I refresh it and try to add another item in the list, the list in the database is erased, and overwritten. How can I fix this?
constructor(props){
super(props);
this.state = {
list:[],
};
}
onSubmitL = e => {
e.preventDefault();
const db = firebase.firestore();
var change = this.state.list;
//this.state.list.push(this.state.temp);
var textbox = document.getElementById("list");
var temp = textbox.value;
if (temp.length == 0) {
console.log("input empty!");
} else {
this.state.list.push(temp);
}
console.log("current " + this.state.list);
db.collection("users").where("email", "==", firebase.auth().currentUser.email)
.get()
.then(snapshots => {
snapshots.forEach(doc => {
const docData = doc.data();
doc.ref.update({
list: this.state.list,
});
})
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
this.state.item="";
};
updateList = e => {
};
render () {
const { currentUser } = this.state
return (
<div>
<form onSubmit={this.onSubmitL}>
<input
name="topics"
id="list"
onChange={this.onChange}
type='list'
placeholder="list"
onChange={this.updateList}
/>
<button type="submit">Apply Changes</button>
</form>
<div>
</div>
</div>
);
}
I dont know if this is the only problem but it should be
snapshots.docs.forEach(doc => { ...
not
snapshots.forEach(doc => { ...
you might be able to try using a ... doc(doc.id)set()with { merge: true }
db.collection("users").where("email", "==",
firebase.auth().currentUser.email)
.get()
.then(snapshots => {
snapshots.docs.forEach(doc => {
const docData = doc.data();
doc.ref.update({
list: this.state.list,
});
})
})
see https://firebase.google.com/docs/firestore/manage-data/add-data?authuser=0
here is my componentDidmount
componentDidMount() {
for ( var i in course ) {
let title = course[i];
const ref = firestore.collection('courses');
ref.where("class", "array-contains", course[i]).get()
.then(querySnapshot => {
const count = querySnapshot.size
course_stats.push({
title: title,
value: count,
});
});
}
console.log(course_stats)
this.setState({
courses: course_stats,
})
}
here is my render
render() {
const { classes } = this.props;
if (this.state.courses) {
console.log(this.state.courses)
return (
<ul>
{course_stats.map(d => <li key={d.title}>{d.title}</li>)}
</ul>
)
}
on the line console.log, I can see the object array in that. However, when i try render that, it doesn't show anything.
this is the console.log capture
how can I render the title and value of array?
Thank you!
Adding to izb's answer, this.setState has already executed, so you should use async/await, or add a seperate callback function like this that returns a Promise
setAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
handleChange = (event) => {
return this.setAsync({[event.target.name]: event.target.value})
}