How to set state of cart when no items? - javascript

In my E-Commerce project, the products are stored in localStorage. componentDidMount() gets all these products from localStorage and displays it. How to state the state or condition when no products are available.
componentDidMount = () => {
this.setProducts();
// Gets all products in cart from localStorage
this.setState(
() =>{
return {cart: JSON.parse(localStorage.getItem('myCart'))}
},
() => {
this.addTotal()
})
// How to set the condition when no products in Cart?
}
// Set all the products on Main Page
setProducts = () => {
let tempProducts = [];
storeProducts.forEach(item => {
const singleItem = {...item};
tempProducts = [...tempProducts, singleItem];
});
this.setState(() => {
return {products: tempProducts};
});
};
// Here products are added to cart and stored in localStorage
addToCart = (id) => {
let tempProducts = [...this.state.products];
const index = tempProducts.indexOf(this.getItem(id));
const product = tempProducts[index];
product.inCart = true;
product.count = 1;
const price = product.price;
product.total = price;
this.setState(() => {
return { products: tempProducts, cart: [...this.state.cart,
product] };
},
() => {
this.addTotal();
localStorage.setItem('myCart', JSON.stringify(this.state.cart))
});
}
I have also tried to make following changes, but, no effect. In componentDidMount()
componentDidMount() {
if(this.state.cart.length > 0) {
this.setState(
() =>{
return {cart: JSON.parse(localStorage.getItem('myCart'))}
},
() => {
this.addTotal()
})
} else {
this.setState(() => {
return {cart:[]}
})
}
}
// Clear Cart
clearCart = () => {
this.setState(() => {
return {cart:[]}
}, () => {
this.setProducts();
this.addTotal();
})
localStorage.removeItem('myCart')
}
When I remove code of setState (shown in the beginning) from componentDidMount() displays empty cart message, which is expected else, if the cart is cleared and refreshed browser throws 'cart.length' error. Any possible solution?

JSON.parse will return an object. It depends on your data structure but there is no cart.lendth for the object. So that is your first problem. So for the below example, I store the parsed value as an array.
Also, if state.cart is not initiated, there is no .length property for it.
For your second problem have a look at the below version of your componentDidMount:
componentDidMount() {
if(Array.isArray(this.state.cart) && this.state.cart.length) {
const cart = localStorage.getItem('myCart') || [];
this.setState({cart: [JSON.parse(cart)]}), this.addTotal);
} else {
this.setState({ cart:[] });
}
}
Again, it depends on your implementation, but you might need to initiate the component's state with cart: localStorage.getItem('myCart') || [] or doing what I have done above. I'm basically checking if cart is an array && it has length then parse it otherwise initiate the array.

Finally I got the solution as below
const cart = localStorage.getItem('myCart')
this.setState({cart: JSON.parse(cart) ? JSON.parse(cart) : []}, this.addTotal)
Just modified the code and works perfectly without any issues

Related

React-native-gifted-chat with cloud firestore pagination

I'm using Firestore to store messages. In order to optimize the mobile application performances, I would like to set a limit(50) in the firestore query.
It works well and implemented the onLoadEarlier React-native-gifted-chat available in the props.
All is working fine.
But, when I send a new message in the chat, after scrolled up to see the earliers messages, only the 50 last messages with the new one, off course, are available.
So, each time I'm adding a message in the Firestore database, the onSnapshot (in the useeffect) is executed and apply the limit query.
Is there a way to avoid this ?
Thanks.
Here my useEffect :
useEffect(() => {
const messagesListener = firestore()
.collection('groups')
.doc(group._id)
.collection('messages')
.orderBy('createdAt', 'desc')
.limit(50)
.onSnapshot(querySnapshot => {
const newMessages = querySnapshot.docs.map(doc => {
const firebaseData = doc.data();
const data = {
_id: doc.id,
text: '',
createdAt: new Date().getTime(),
...firebaseData
};
return data;
});
setMessages(previousMessages => {
return GiftedChat.append(previousMessages, newMessages);
});
});
return () => messagesListener();
}, []);
I am using FlatList in react-native to render chats and I had to paginate the chats list. Since Firestore query cursor is not supported in live listener, I created two list, recentChats & oldChats.
I populate recentChats using live listener query.onSnapshot & oldChats using cursor startAfter. FlatList data is combination of both list and I take care of merging logic.
const MESSAGE_LIMIT = 15;
const ChatWindow = props => {
const { sessionId, postMessage, onSendTemplateButtonPress } = props;
// Firestore cursor is not supported in query.onSnapshot so maintaining two chat list
// oldChats -> chat list via cursor, recentChats -> chat list via live listener
const [oldChats, setOldChats] = useState([]);
const [recentChats, setRecentChats] = useState([]);
// if true, show a loader at the top of chat list
const [moreChatsAvailable, setMoreChatsAvailable] = useState(true);
const [inputMessage, setInputMessage] = useState('');
useEffect(() => {
const query = getGuestChatMessagesQuery(sessionId)
.limit(MESSAGE_LIMIT);
const listener = query.onSnapshot(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
// merge recentChats & chats
if (recentChats.length > 0) {
const newRecentChats = [];
for (let i = 0; i < chats.length; i++) {
if (chats[i].sessionId === recentChats[0].sessionId) {
break;
}
newRecentChats.push(chats[i]);
}
setRecentChats([...newRecentChats, ...recentChats]);
} else {
setRecentChats(chats);
if (chats.length < MESSAGE_LIMIT) {
setMoreChatsAvailable(false);
}
}
});
return () => {
// unsubscribe listener
listener();
};
}, []);
const onMessageInputChange = text => {
setInputMessage(text);
};
const onMessageSubmit = () => {
postMessage(inputMessage);
setInputMessage('');
};
const renderFlatListItem = ({ item }) => {
return (<ChatBubble chat={item} />);
};
const onChatListEndReached = () => {
if (!moreChatsAvailable) {
return;
}
let startAfterTime;
if (oldChats.length > 0) {
startAfterTime = oldChats[oldChats.length - 1].time;
} else if (recentChats.length > 0) {
startAfterTime = recentChats[recentChats.length - 1].time;
} else {
setMoreChatsAvailable(false);
return;
}
// query data using cursor
getGuestChatMessagesQuery(sessionId)
.startAfter(startAfterTime)
.limit(MESSAGE_LIMIT)
.get()
.then(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
if (chats.length === 0) {
setMoreChatsAvailable(false);
} else {
setOldChats([...oldChats, ...chats]);
}
});
};
return (
<View style={[GenericStyles.fill, GenericStyles.p16]}>
<FlatList
inverted
data={[...recentChats, ...oldChats]}
renderItem={renderFlatListItem}
keyExtractor={item => item.messageId}
onEndReached={onChatListEndReached}
onEndReachedThreshold={0.2}
ListFooterComponent={moreChatsAvailable ? <ActivityIndicator /> : null}
/>
{
Singleton.isStaff ?
null:
<ChatInput
onMessageInputChange={onMessageInputChange}
onMessageSubmit={onMessageSubmit}
inputMessage={inputMessage}
style={GenericStyles.selfEnd}
onSendTemplateButtonPress={onSendTemplateButtonPress}
/>
}
</View>
);
};
Your query is OK for the first time, for consequent queries you must use the ::startAt or ::startAfter methods.
You can find more information in the official documentation.
https://firebase.google.com/docs/firestore/query-data/query-cursors

How can I persist localstorage when ComponentDidMount resets the value on refresh?

I am appending an item to each array item when fetching from a list and setting it to zero.
Then running the function in ComponentDidMount().
newsList = (category) => {
fetch(API_URL)
.then((response) => response.json())
.then((response) => {
var news = response.articles;
news = news.map((x) => ({ ...x, likes: 0 }));
this.setState({ general: news }, () => {
localStorage.setItem("general", JSON.stringify(this.state.general));
});
});
};
I am subsequently incrementing the 'like' value I appended and saving to local storage.
generalLikeCount = (x) => {
let newArr = [...this.state.general];
newArr[x]["likes"] = parseInt(newArr[x]["likes"]) + 1;
this.setState({ general: newArr }, () => {
localStorage.setItem("general", JSON.stringify(this.state.general));
});
};
But on every refresh, likes counter is reset to zero. I'm not sure how to fix this.
In case anyone has the same issue, I fixed this by checking if the value exists in local storage and using that value instead, or initializing it if it doesn't exist.
Like so:
if(JSON.parse(localStorage.getItem(category)) !== null || undefined ) {
news = JSON.parse(localStorage.getItem(category));
} else {
news = news.map((x) => ({ ...x, likes: 0 }));
}

Vue array computed via parameterized getter not reactive

I have a list a discussion object containing an array of comments and each comment can hold an array of replies. I display the discussion this way:
<div v-for="comment in comments" v-bind:key="comment._id">
<Comment :itemId="itemId" :comment="comment" />
<Replies v-if="comment.replies.length > 0" :itemId="itemId" :comment="comment" />
</div>
<Button value="Load more" #clicked="loadMoreComments(itemId)" />
and Replies:
<div v-for="reply in replies" v-bind:key="reply._id">
<Comment :itemId="itemId" :comment="reply" />
</div>
<Button :value="Load more" #clicked="loadChild()"/>
As you can see both use the same pattern. They differ in a computed property:
computed: {
comments() {
return this.$store.getters.DISCUSSION.comments.map(id => this.$store.getters.GET_COMMENT(id));
},
replies() {
return this.$store.getters.GET_REPLIES(this.comment).map(id => this.$store.getters.GET_COMMENT(id));
},
},
When I hit the Load more button for comments, new comments appear. But when I hit the Load more button in replies, then no new reply is displayed though I can see in debugger that the array was enlarged.
Vuex store submodule:
state: () => ({
discussion: {
incomplete: true,
comments: [],
},
comments: {},
}),
getters: {
DISCUSSION: state => state.discussion,
GET_COMMENT: state => id => state.comments[id],
GET_REPLIES: state => (comment) => {
if (comment.allShown) {
return comment.replies;
}
return comment.replies.slice(0, REPLY_LIMIT);
},
},
mutations: {
APPEND_COMMENTS: (state, payload) => {
const { comments, incomplete, userId } = payload;
state.discussion.incomplete = incomplete;
const commentIds = [];
comments.forEach(comment => processComment(state, comment, commentIds, userId));
state.discussion.comments = state.discussion.comments.concat(commentIds);
},
PREPEND_COMMENTS: (state, payload) => {
const { comments, userId } = payload;
const commentIds = [];
comments.forEach(comment => processComment(state, comment, commentIds, userId));
state.discussion.comments = commentIds.concat(state.discussion.comments);
},
SET_REPLIES: (state, payload) => {
console.log('SET_REPLIES');
const { commentId, replies, userId, replace } = payload;
const comment = state.comments[commentId];
if (!comment) {
return;
}
state.comments[commentId].showAll = true;
const commentIds = [];
replies.forEach(reply => processComment(state, reply, commentIds, userId));
if (!comment.replies || comment.replies.length === 0 || replace) {
state.comments[commentId].replies = commentIds;
} else {
state.comments[commentId].replies = comment.replies.concat(commentIds);
}
},
}
function processComment(state, comment, commentIds, userId) {
if (comment.replies) {
const repliesIds = [];
comment.replies.forEach((reply) => {
reply.voted = hasVoted(reply.votes, userId);
state.comments[reply._id] = reply;
repliesIds.push(reply._id);
});
comment.replies = repliesIds;
comment.allShown = comment.replies.length < REPLY_LIMIT;
} else if (!comment.parentId) {
comment.replies = [];
comment.allShown = false;
}
state.comments[comment._id] = comment;
commentIds.push(comment._id);
}
The complete source code is there: https://github.com/literakl/mezinamiridici/tree/comment_refactoring/spa
Here is minimum reproducible codesandbox: https://codesandbox.io/s/frosty-taussig-v8u4b?file=/src/module.js
I have verified that this happens because of the getter with a parameter. When I put the reply in static array so I could use parameter-less getter, it started to work.
I follow this recommendation: https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143
Where is the issue?
Update:
One thing that smells is the mutation GET_REPLIES because it works on the passed object. So Vue has no chance to detect that the object is from the state. So I have rewritten it to pass only commentId and load the comment from the state, but it did not help.
I guest you should replace showAll with allShown prop and also use Vue.set where you add new keys to comments object because due to Vue caveats Vuex doesn't see new props, see caveats for objects
SET_REPLIES: (state, payload) => {
console.log("SET_REPLIES");
const { commentId, replies, userId, replace } = payload;
const comment = state.comments[commentId];
if (!comment) {
console.log(`Comment ${commentId} not found`);
return;
}
state.comments[commentId].allShown = true;
// state.comments[commentId].showAll = true;
...
function processComment(state, comment, commentIds, userId) {
if (comment.replies) {
const repliesIds = [];
comment.replies.forEach(reply => {
Vue.set(state.comments, reply._id, reply);
// state.comments[reply._id] = reply;
repliesIds.push(reply._id);
});
comment.replies = repliesIds;
comment.allShown = comment.replies.length < 3;
} else if (!comment.parentId) {
comment.replies = [];
comment.allShown = false;
}
Vue.set(state.comments, comment._id, comment);
// state.comments[comment._id] = comment;
commentIds.push(comment._id);
}
Also correct GET_REPLIES call like this:
computed: {
replies() {
return this.$store.getters
.GET_REPLIES(this.comment) // passing comment itself instead of its id
.map(id => this.$store.getters.GET_COMMENT(id));
}
},
corrected example

React Native Flatlist Not Rerendering Redux

My FlatList does not update when the props I pass from redux change. Every time I send a message I increase everyones unread message count in both firebase and in my redux store. I made sure to include key extractor and extra data, but neither helps. The only thing that changes the unread message count is a reload of the device. How do I make sure the flatList updates with MapStateToProps. I made sure to create a new object by using Object.Assign:
action:
export const sendMessage = (
message,
currentChannel,
channelType,
messageType
) => {
return dispatch => {
dispatch(chatMessageLoading());
const currentUserID = firebaseService.auth().currentUser.uid;
let createdAt = firebase.database.ServerValue.TIMESTAMP;
let chatMessage = {
text: message,
createdAt: createdAt,
userId: currentUserID,
messageType: messageType
};
FIREBASE_REF_MESSAGES.child(channelType)
.child(currentChannel)
.push(chatMessage, error => {
if (error) {
dispatch(chatMessageError(error.message));
} else {
dispatch(chatMessageSuccess());
}
});
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child(channelType)
.child(currentChannel).child('users')
UNREAD_MESSAGES.once("value")
.then(snapshot => {
snapshot.forEach(user => {
let userKey = user.key;
// update unread messages count
if (userKey !== currentUserID) {
UNREAD_MESSAGES.child(userKey).transaction(function (unreadMessages) {
if (unreadMessages === null) {
dispatch(unreadMessageCount(currentChannel, 1))
return 1;
} else {
alert(unreadMessages)
dispatch(unreadMessageCount(currentChannel, unreadMessages + 1))
return unreadMessages + 1;
}
});
} else {
UNREAD_MESSAGES.child(userKey).transaction(function () {
dispatch(unreadMessageCount(currentChannel, 0))
return 0;
});
}
}
)
})
};
};
export const getUserPublicChannels = () => {
return (dispatch, state) => {
dispatch(loadPublicChannels());
let currentUserID = firebaseService.auth().currentUser.uid;
// get all mountains within distance specified
let mountainsInRange = state().session.mountainsInRange;
// get the user selected mountain
let selectedMountain = state().session.selectedMountain;
// see if the selected mountain is in range to add on additional channels
let currentMountain;
mountainsInRange
? (currentMountain =
mountainsInRange.filter(mountain => mountain.id === selectedMountain)
.length === 1
? true
: false)
: (currentMountain = false);
// mountain public channels (don't need to be within distance)
let currentMountainPublicChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Public");
// mountain private channels- only can see if within range
let currentMountainPrivateChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Private");
// get public channels
return currentMountainPublicChannelsRef
.orderByChild("key")
.once("value")
.then(snapshot => {
let publicChannelsToDownload = [];
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
// add the channel ID to the download list
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
// flag whether you can check in or not
if (currentMountain) {
dispatch(checkInAvailable());
} else {
dispatch(checkInNotAvailable());
}
// if mountain exists then get private channels/ if in range
if (currentMountain) {
currentMountainPrivateChannelsRef
.orderByChild("key")
.on("value", snapshot => {
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",
snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
});
}
return publicChannelsToDownload;
})
.then(data => {
setTimeout(function () {
dispatch(loadPublicChannelsSuccess(data));
}, 150);
});
};
};
Reducer:
case types.UNREAD_MESSAGE_SUCCESS:
const um = Object.assign(state.unreadMessages, {[action.info]: action.unreadMessages});
return {
...state,
unreadMessages: um
};
Container- inside I hook up map state to props with the unread messages and pass to my component as props:
const mapStateToProps = state => {
return {
publicChannels: state.chat.publicChannels,
unreadMessages: state.chat.unreadMessages,
};
}
Component:
render() {
// rendering all public channels
const renderPublicChannels = ({ item, unreadMessages }) => {
return (
<ListItem
title={item.info.Name}
titleStyle={styles.title}
rightTitle={(this.props.unreadMessages || {} )[item.id] > 0 && `${(this.props.unreadMessages || {} )[item.id]}`}
rightTitleStyle={styles.rightTitle}
rightSubtitleStyle={styles.rightSubtitle}
rightSubtitle={(this.props.unreadMessages || {} )[item.id] > 0 && "unread"}
chevron={true}
bottomDivider={true}
id={item.Name}
containerStyle={styles.listItemStyle}
/>
);
};
return (
<View style={styles.channelList}>
<FlatList
data={this.props.publicChannels}
renderItem={renderPublicChannels}
keyExtractor={(item, index) => index.toString()}
extraData={[this.props.publicChannels, this.props.unreadMessages]}
removeClippedSubviews={false}
/>
</View>
);
}
}
Object.assign will merge everything into the first object provided as an argument, and return the same object. In redux, you need to create a new object reference, otherwise change is not guaranteed to be be picked up. Use this
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
// or
const um = {...state.unreadMessages, [action.info]: action.unreadMessages }
Object.assign() does not return a new object. Due to which in the reducer unreadMessages is pointing to the same object and the component is not getting rerendered.
Use this in your reducer
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});

How can i modifier a state only when has the same ID

editItem = (id) => {
this.setState(
{
lanche: [
{
id: id,
editItem: true
}]
}
)
}
In this way, setState runs on all items in the array, I need to edit only the item that has the same ID
You need to find the item in the array with the given id, and modify that one only. setState method changes the whole object with the given key.
try this:
editItem = (id) => {
this.setState(
{
lanche: this.state.lanche.map(item => {
if(item.id === id) {
item.editItem = true
}
return item;
})
}
)
}
You can use map like this:
editItem = (id) => {
// update the relevant item
const updatedItems = this.state.lanche.map(item => {
if (item.id === id) {
item.editItem = true;
}
return item;
} );
this.setState(
{
lanche: updatedItems
}
)
}
If I understand your question;
You could search for the index like so;
const index = this.state.lanche.findIndex((e) => e.id === id);
and then alter the object
const newLanche = this.state.lanche;
newLanche[index].editItem = true;
And then update the store;
this.setState({ lanche: newLanche });

Categories

Resources