Vue.js: How to avoid error after updating store? - javascript

I am building an app with vue.js and vuex that stores entries of household accounts (i.e. personal expenses and incomes). The entries are filtered by month; a slider can be used to change months. The current code is on Github.
Displaying existing entries works fine. The problem arises when I try to add a new entry. When I add an entry for an existing month, it works fine. But when I add an entry for a new month (i.e. a month for which no other entry exists yet), I get a myterious warning and a myterious error: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. Note: When I say month I mean month-year combination.
Any ideas where this error might be coming from?
Here is some more info about the current app. The current application looks like this:
My store looks like this:
const store = createStore({
state() {
return {
categories: [
{
type: "expense",
key: "living",
value: "Wohnen",
},
...
],
entries: [
{
id: 1,
date: "2020-10-15T14:48:00.000Z",
type: "expense",
category: "Lebensmittel",
amount: 15.24,
note: "Edeka",
},
...
],
};
},
getters: {
sorted_entries(state) {
// Returns all entries, sorted by date
...
return sorted_entries;
},
categories(state) {
return state.categories;
},
entriesMonths(state) {
// Return an ordered arrays of all months present among entries
// Ex.:{index: 0, month: "11", year: "2019", displayMonth: "November 2019"}
...
return entriesMonths;
},
},
mutations: {
addEntry(state, entryData) {
const newEntry = {
id: new Date().toISOString(),
date: entryData.date,
type: entryData.type,
category: entryData.category,
amount: entryData.amount,
note: entryData.note,
};
console.log("right before push")
state.entries.push(newEntry);
},
},
actions: {
addEntry(context, entryData) {
context.commit("addEntry", entryData); // Place to store in backend server
},
},
});
In EntryList.vue I get sorted_entries and entriesMonths from the store:
sorted_entries is used to calculate filtered_entries which filters the entries according to what month is currently displayed by the slider (initialization: latest month among entries in store). filtered_entries is displayed as a list
entriesMonths is passed to another component MonthSelector.vue which implements the slider to change months and emits the new month after a change to EntryList.vue so it can update filtered_entries.
New entries are added via NewEntryForm.vue. This component includes a form to ask the user for the new entry data. It then emits this data as an object to its parent AddEntry.vue which in turn sends it to the store. There an action triggers a mutation which adds the entry to the state.
Of cause there are a lot of more details to mention, but I don't know which are relevant to solving this problem. Please ask, if you need more info or have a look at the code (Github).
Many thanks!

The problem is caused by sending the entriesMonths as a property to MonthSelector.vue component,
By that you're violating the purpose of having a store/getters/mutations, you can access the entriesMonths directly from your component, you don't have to pass it as a prop,
So change your MonthSelector.vue as follows:
Remove the property that is passed to the component
In your slideOpts.initialSlide access the entriesMonths directly from the store.
initialSlide: this.$store.getters.entriesMonths.length - 1
*Update: *
Please remove the entriesMonths from the computed properties and put it in data as follows:
data() {
const entriesMonths = this.$store.getters.entriesMonths
return {
entriesMonths,
slideOpts: {
initialSlide: entriesMonths.length - 1, // Start with newest slide/month
speed: 300,
pagination: false,
},
};
},

Related

How to use spread operator in setstate react class component

I am developing a component where I will get the data from a call back function. Initially the state of the component will be empty [], later once the callback function is called I need to update the values into the state. At a time I'll recive only one array, meaning user can add one item at a time that item will consists of nested objects and array values. I have added the logic for the same to handle the scenario, but when I am testing in jest when I am trying to add another set of item from mock meaning the user can select next item when the done with selecting and submitting the first item at that time my logic is getting failed, I am not getting where I went wrong, could any one help me to resolve this issue, thanks in advance! I have added the mock data structure and logic and jest test below.
Mock:
const items = {
itemList: {
itemOne: [{
id: "01",
category: "It-A",
isCreated:"true"
}],
itemDesc:[{
id:"01",
type:"A-1",
isCreated:"true"
}]
}
ItemID:'123'
}
Code:
class ItemComp extends React.Component{
this.state = {
processingItems:[]
onAddItemHandle = (processingItem) => {
this.setState(prevState => ({
processingItems: [...prevState.processingItems, processingItem]
}))
}
JEST:
describe('handleonAddItem', () => {
it('should allow to add multiple items based on prevState', () => {
const compView = mountWithIntl(
<compView
itemId={12}
/>
}
const instance = compView.find(compViewComponent).instance();
instance.onAddItemHandle(items) // when I am giving only one instance my logic is working
instance.onAddItemHandle(items) //when I am giving it for second time it's failing I am getting error like expected - 0 , received +18 I want to update the items here when user clicks for second time but it is failing.
expect(instance.state.processingItems).toEqual([items])
Missing a ',' before the ItemID is the only issue I faced while reproducing.- https://codesandbox.io/s/intelligent-chaplygin-0ot56e?file=/src/App.js
const items = {
itemList: {
itemOne: [{
id: "01",
category: "It-A",
isCreated:"true"
}],
itemDesc:[{
id:"01",
type:"A-1",
isCreated:"true"
}]
},
ItemID:'123'
}

Mongodb findOneAndUpdate - check if new document added, or just updated

I have the following code which is finding and updating an employee-store record (if it exists, otherwise it creates one).
I have lots of employees in different stores, and they can choose to change store at any point (as long as the location is in America)
Below is my code so far:
employee = await this.employeeStoreModel.findOneAndUpdate(
{ employee: employeeRecord._id, location: "America" },
{
employee: employeeRecord._id,
store: employeeRecord.store,
location: "America",
},
{ new: true, upsert: true }
);
This works correctly, however I am trying to return some messages from this based on what is being updated. It could be any of the following messages:
If it's a completely new employee-store record being added, then return "{{StoreID}} has a new employee - {{EmployeeID}}"
If it's a change of store on an existing employee-store record, then return "{{EmployeeID}} has changed from {{old StoreID}} to {{new StoreID}}"
Is this possible to do? Can anyone guide me on how I could start this?
In the Options field,
rawResult: true
You must add the parameter.
The result will be as in the example below.
{ response:
{ n: 1,
updatedExisting: false,
upserted: 5e6a9e5ec6e44398ae2ac16a },
value:
{ _id: 5e6a9e5ec6e44398ae2ac16a,
name: 'Will Riker',
__v: 0,
age: 29 },
ok: 1 }
Depending on whether there is an update or insert in the updatedExisting field, you can return any message you want.

How does fetchMore return data to the component?

I am trying to follow the example of cursor-based paginating with React Apollo (https://www.apollographql.com/docs/react/data/pagination/#cursor-based) but am struggling with how my component that rendered the original data gets the new (appended) data.
This is how we get the original data and pass it to the component:
const { data: { comments, cursor }, loading, fetchMore } = useQuery(
MORE_COMMENTS_QUERY
);
<Comments
entries={comments || []}
onLoadMore={...}
/>
What I'm unsure of is how the fetchMore function works.
onLoadMore={() =>
fetchMore({
query: MORE_COMMENTS_QUERY,
variables: { cursor: cursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const previousEntry = previousResult.entry;
const newComments = fetchMoreResult.moreComments.comments;
const newCursor = fetchMoreResult.moreComments.cursor;
return {
// By returning `cursor` here, we update the `fetchMore` function
// to the new cursor.
cursor: newCursor,
entry: {
// Put the new comments in the front of the list
comments: [...newComments, ...previousEntry.comments]
},
__typename: previousEntry.__typename
};
}
})
}
From what I understand, yes, once my component will cal this onLoadMore function (using a button's onClick for example), it will fetch the data based on a new cursor.
My question is this. I'm sorry if this is too simple and I'm not understanding something basic.
How does the component get the new data?
I know the data is there, because I console logged the newComments (in my case, it wasn't newComments, but you get the idea.) And I saw the new data! But those new comments, how are they returned to the component that needs the data? And if I click the button again, it is still stuck on the same cursor as before.
What am I missing here?
In the updateQuery function lets you modify (override) the result for the current query. At the same time your component is subscribed to the query and will get the new result. Let's play this through:
Your component is rendered for the first time, component will subscribe to the query and receive the current result of the query from the cache if there is any. If not the query starts fetching from the GraphQL server and your component gets notified about the loading state.
If the query was fetched your component will get the data once the result came in. It now shows the first x results. In the cache an entry for your query field is created. This might look something like this:
{
"Query": {
"cursor": "cursor1",
"entry": { "comments": [{ ... }, { ... }] }
}
}
// normalised
{
"Query": {
"cursor": "cursor1",
"entry": Ref("Entry:1"),
}
"Entry:1": {
comments: [Ref("Comment:1"), Ref("Comment:2")],
},
"Comment:1": { ... },
"Comment:2": { ... }
}
User clicks on load more and your query is fetched again but with the cursor value. The cursor tells the API from which entry it should start returning values. In our example after Comment with id 2.
Query result comes in and you use the updateQuery function to manually update the result of the query in the cache. The idea here is that we want to merge the old result (list) with the new result list. We already fetched 2 comments and now we want to add the two new comments. You have to return a result that is the combined result from two queries. For this we need to update the cursor value (so that we can click "load more" again and also concat the lists of comments. The value is written to the cache and our normalised cache now looks like this:
{
"Query": {
"cursor": "cursor2",
"entry": { "comments": [{ ... }, { ... }, { ... }, { ... }] }
}
}
// normalised
{
"Query": {
"cursor": "cursor2",
"entry": Ref("Entry:1"),
}
"Entry:1": {
comments: [Ref("Comment:1"), Ref("Comment:2"), Ref("Comment:3"), Ref("Comment:4")],
},
"Comment:1": { ... },
"Comment:2": { ... },
"Comment:3": { ... },
"Comment:4": { ... }
}
Since your component is subscribed to the query it will get rerendered with the new query result from the cache! The data is displayed in the UI because we merged the query so that the component gets new data just as if the result had all four comments in the first place.
It depends on how you handle the offset. I'll try to simplify an example for you.
This is a simplified component that I use successfully:
const PlayerStats = () => {
const { data, loading, fetchMore } = useQuery(CUMULATIVE_STATS, {
variables: sortVars,
})
const players = data.GetCumulativeStats
const loadMore = () => {
fetchMore({
variables: { offset: players.length },
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
return {
...prevResult,
GetCumulativeStats: [
...prevResult.GetCumulativeStats,
...fetchMoreResult.GetCumulativeStats,
],
}
},
})
}
My CUMULATIVE_STATS query returns 50 rows by default. I pass the length of that result array to my fetchMore query as offset. So when I execute CUMULATIVE_STATS with fetchMore, the variables of the query are both sortVars and offset.
My resolver in the backend handles the offset so that if it is, for example, 50, it ignores the first 50 results of the query and returns the next 50 from there (ie. rows 51-100).
Then in the updateQuery I have two objects available: prevResult and fetchMoreResult. At this point I just combine them using spread operator. If no new results are returned, I return the previous results.
When I have fetched more once, the results of players.length becomes 100 instead of 50. And that is my new offset and new data will be queried the next time I call fetchMore.

Redux Form needs to display my filtered values in the browser

Using Redux Form I'm able to retrieve the values which I enter in username. I'm able to filter out the corresponding values, but I need to display my filtered values in the browser.
console.log("pilot.name--->", pilot.name);
Can you tell me how to do it? I provided my code snippet and sandbox below. My related code is in showResults.js: https://codesandbox.io/s/xl1r14w854.
var pilots = [
{
id: 2,
name: "Wedge Antilles",
faction: "Rebels"
},
{
id: 8,
name: "Ciena Ree",
faction: "Empire"
},
{
id: 40,
name: "Iden Versio",
faction: "Empire"
},
{
id: 66,
name: "Thane Kyrell",
faction: "Rebels"
}
];
var rebels = pilots.filter(function(pilot) {
// return pilot.faction === "Rebels";
// return pilot.faction === values.username;
if (pilot.faction === values.username) {
console.log("pilot.name--->", pilot.name);
}
});
I suggest to use a separate reducer for this. Imagine that you call that reducer PilotsReducer where you have your list of pilots in the state. That reducer is "listening" to a specific action like loginSubmitted that you trigger when you get the response from the server.
The payload of that action will be the username that you use to do the filter. In the reducer you can then do the filter and set a state property (e.g. rebels) to the result of the filter. Your component can then be attached to redux to pick the rebels property and it will re-render when that property changes.
This is a typical flow of react/redux, it is not specific to your example
On a side note you can improve it by using reselect so you would keep the entire list of pilots in the state, and filter them in a selector by passing the current username to it. Please check the docs and tutorials of redux and reselect to have a clear view of the entire workflow.

Enyo Collection Merge Strategy

Below is the function I have used to fetch more photos from a service provider once clicked on more button:
showMore: function(){
this.$.resultList.collection.fetch({strategy:"merge",rpp:50});
}
This will call the fetch method defined in collection,
fetch: function(opts) {
this.params = {
feature: this.methodType || "popular",
image_size: 3,
sort: "created_at",
rpp: opts && opts.rpp || 25
};
return this.inherited(arguments);
},
This is working fine, but the problem is once more button is clicked, it should fetch another set of 25 photos and append it to my collection, but what actually happening is sometimes, it shows only say 5 photos along with previous photos .
What I understand by "merge" strategy is, if the records received is same as previous records, it will take only those records which are different from previously fetched records and updates the primarykey of the duplicate records.So one reason i am able to figure out is that, may be, it is fetching 25 photos next time, but because most of them are same as before, it is showing only those which are different from the previous fetched photos.
If i go with the "add" strategy, it works fine for one time, i.e, it shows another set of 25 photos appended to the collection, most of them are again same. But if click on more button one more time, no records are being added to the collection.No idea why this is happening.
How should i approach, if i want to fetch only new photos and append it to the collection.
Using the merge strategy is the right approach. Your description of merge is mostly accurate except that it doesn't update the primary key but instead updates the data for existing records with the same primary key.
It's difficult to say why using "add" doesn't always work. If the records don't have a primary key (which is id by default), "add" and "merge" should always add the records to the collection (unless you're using mergeKeys). If they do have a primary key, it's possible that you're trying to add duplicate records which Enyo will complain about and abort. Check your console log.
Without code, the only other suggestion is to set breakpoints and step through enyo.Collection.merge.
Here's an example of fetching records into a collection. If you comment out setting the id, merge and add strategies will always add records. If you comment out the merge strategy, the code will eventually error when requesting more records.
enyo.kind({
name: "ex.MockSource",
kind: "enyo.Source",
fetch: function(rec, opts) {
if(rec instanceof enyo.Model) {
rec.setObject(Faker.Helpers.createCard());
} else if (rec instanceof enyo.Collection) {
var count = opts && opts.count || 25;
var cards = [];
for(var i=0;i<count;i++) {
var card = Faker.Helpers.createCard();
// artificial id to force merges
card.id = enyo.now()%40;
cards.push(card);
}
opts.success(cards);
}
}
});
enyo.store.addSources({
mock: "ex.MockSource"
});
enyo.kind({
name: "ex.App",
kind: "Scroller",
bindings: [
{from: ".data", to: ".$.list.collection"},
{from: ".data.length", to: ".$.count.content", transform: function(v) {
return enyo.format("Displaying %s records", v);
}}
],
components: [
{name: "count"},
{name: "list", kind: "DataRepeater", components: [
{kind: "onyx.Item", components: [
{name: "name"},
{name: "phone"}
], bindings: [
{from: ".model.name", to: ".$.name.content"},
{from: ".model.phone", to: ".$.phone.content"}
]}
]},
{kind: "onyx.Button", content: "More", ontap: "moreTapped"}
],
create: enyo.inherit(function(sup) {
return function() {
sup.apply(this, arguments);
this.set("data", new enyo.Collection({
defaultSource: "mock"
}));
this.fetchRecords();
};
}),
fetchRecords: function() {
this.data.fetch({
count: 5,
strategy: "merge"
});
},
moreTapped: function() {
this.fetchRecords();
}
});
new ex.App().renderInto(document.body);

Categories