Vue array computed via parameterized getter not reactive - javascript

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

Related

The filter function does not remove the object in the array in Vue/Vuex

I am busy with making a booking function in vue3/vuex.
A user can book an item and also remove it from the basket.The problem is that the Filter function in vue does not remove the object in the array and I can not find out what the problem is. I hope you can help me
This is the result if I put console.log() in the removeFromBasket(state, payload)
removeFromBasket(state, payload) {
console.log('removeFromBasket', payload, JSON.parse(JSON.stringify(state.basket.items)))
}
method to remove
removeFromBasket() {
this.$store.commit('basket/removeFromBasket', this.bookableId);
}
basket module
const state = {
basket: {
items: []
},
};
const getters = {
getCountOfItemsInBasket: (state) => {
return state.basket.items.length
},
getAllItemsInBasket: (state) => {
return state.basket.items
},
inBasketAlready(state) {
return function (id) {
return state.basket.items.reduce((result, item) => result || item.bookable.id === id, false);
}
},
};
const actions = {};
const mutations = {
addToBasket(state, payload) {
state.basket.items.push(payload);
},
removeFromBasket(state, payload) {
state.basket.items = state.basket.items.filter(item => item.bookable.id !== payload);
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
I have solved the problem.
I used typeof() in console.log() to see what type the payload and item.bookable.id are.
The payload was a string and the item.bookable.id was a number.
So I put the payload in parseInt(payload) and the problem was solved.
removeFromBasket(state, payload) {
state.basket.items = state.basket.items.filter(item => item.bookable.id !== parseInt(payload));
}

how to check if all the keys of a immutable map have value

const [fieldValues, updateFieldValues] = useState(fromJS({
imageName: '',
tags: [],
password: '',
}));
const handleButtonClick = () => {
const res = // logic to find which field/fields are empty
}
how can i validate if all the keys have value ?
p.s: i have the following requirements.
imageName and password should have at least one character.
tags should have at least one item.
You can use Yup a javascript library to vaidate values using a pre defined schema. Or else you can write a function that returns the keys of object that doesnt meet requirements
CUSTOM JS METHOD
function validateObj(obj) {
let isValid = true;
const errorKeys = [];
Object.entries(obj).forEach(([key, value]) =>{
if(!value) {
errorKeys.push(key)
isValid = false;
return;
}
if(Array.isArray(value)) {
if(!value?.length) {
errorKeys.push(key)
isValid = false;
return;
}
}
})
return { isValid, errorKeys }
}
IMMUTABLE JS METHOD
function validateObj(obj) {
let isValid = true;
const errorKeys = [];
console.log(obj)
obj.mapEntries(([key, value]) =>{
console.log(key, value)
if(!value) {
errorKeys.push(key)
isValid = false;
return;
}
if(List.isList(value)) {
if(!value.size) {
errorKeys.push(key)
isValid = false;
return;
}
}
})
return { isValid, errorKeys }
}
USAGE:
const handleButtonClick = () => {
const { isValid, errorKeys } = validateObj(obj);
if(!isValid) {
// SHOW ERROR MESSAGE HERE
}
}
Please feel free to reach out in case of any issues/ doubts
EDIT: Link to example fiddle
Immutable.js Collections are Iterables, so you can use for ... of and other common interactions on them. It also offers many ways of looking at the data, like forEach, find, findKey, findValue, filter. Take a look at the docs, they are good. A few examples can be seen below:
const thing = Immutable.fromJS({
banana: 'scale',
imageName: '',
tags: [],
password: '',
});
for([key, val] of thing) {
console.log('for of', key, ':', val);
}
thing.forEach((key, val) => {
console.log('forEach', key, ':', val);
});
const firstItemIdoNotLike = thing.findKey((key, val) => {
if(!!val) {
return true;
}
if (val.hasOwnProperty('length')) {
return val.length < 1;
}
return false;
});
if (firstItemIdoNotLike) {
console.log('validation error on', firstItemIdoNotLike);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/4.0.0-rc.12/immutable.js"></script>

Trying to access state in oncompleted method

I have API query and getting the result and setting those in a state variable in Oncompleted method of API query, Now i am updating the same state variable in another api query "onCompleted method.
I am not able to access the result from state what i have set before in first api query and below is my code
Query 1:
const designHubQueryOnCompleted = designHubProject => {
if (designHubProject) {
const {
name,
spaceTypes
} = designHubProject;
updateState(draft => { // setting state here
draft.projectName = name;
draft.spaceTypes = (spaceTypes || []).map(po => {
const obj = getTargetObject(po);
return {
id: po.id,
name: obj.name,
category: obj.librarySpaceTypeCategory?.name,
description: obj.description,
warning: null // trying to modify this variable result in another query
};
});
});
}
};
const { projectDataLoading, projectDataError } = useProjectDataQuery(
projectNumber,
DESIGNHUB_PROJECT_SPACE_TYPES_MIN,
({ designHubProjects }) => designHubQueryOnCompleted(designHubProjects[0])
);
Query 2:
const {
// data: designhubProjectSpaceTypeWarnings,
loading: designhubProjectSpaceTypeWarningsLoading,
error: designhubProjectSpaceTypeWarningsError
} = useQuery(DESIGNHUB_PROJECT_LINKED_SPACETYPE_WARNINGS, {
variables: {
where: {
projectNumber: { eq: projectNumber }
}
},
onCompleted: data => {
const projectSpaceTypeWarnings = data.designHubProjectLinkedSpaceTypeWarnings[0];
const warnings = projectSpaceTypeWarnings.spaceTypeWarnings.reduce((acc, item) => {
const spaceTypeIdWithWarningState = {
spaceTypeId: item.spaceTypeProjectObjectId,
isInWarningState: item.isInWarningState
};
acc.push(spaceTypeIdWithWarningState);
return acc;
}, []);
console.log(state.spaceTypes); // trying to access the state here but getting empty array
if (state.spaceTypes.length > 0) {
const updatedSpaceTypes = state.spaceTypes;
updatedSpaceTypes.forEach(item => {
const spaceTypeWarning = { ...item };
spaceTypeWarning.warning = warnings?.filter(
w => w.spaceTypeId === spaceTypeWarning.id
).isInWarningState;
return spaceTypeWarning;
});
updateState(draft => {
draft.spaceTypes = updatedSpaceTypes;
});
}
}
});
Could any one please let me know where I am doing wrong with above code Or any other approach to modify the state, Many thanks in advance!!

How to set state of cart when no items?

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

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});

Categories

Resources