I'm using React with Meteor and am having trouble keeping my data updated. Here is my getMeteorData() code in a Conversation component
getMeteorData() {
var vertices_handle = Meteor.subscribe('VertexIDs', this.props.conversation_id);
return {
vertices: Vertices.find({conversation: this.props.conversation_id}).fetch(),
ready: vertices_handle.ready()
};
}
The subscription only returns the IDs of the posts (vertices) and I use this data to render more components:
renderPostList() {
return this.data.vertices.map((post) => {
return <PostThread
key = {post._id}
root_id = {post._id}
conversation_id = {this.props.conversation_id} />;
});
}
Within the PostThread component I subscribe to each post individually by its ID to get the rest of the data as needed. However, when I remove something from the Vertices collection, the Conversation component doesn't seem to be updating. I can see in MeteorToys that the Vertices collection on the client has removed a post, but this change sometimes isn't reflected in the UI. Sometimes when a post is removed the UI updates correctly but other times it doesn't and I have not been able to find a pattern to this.
Related
I have a component that renders a table of Inventoried computer equipment. Here is the relevant code for initial render:
let oEquiptByType = reactive({
Laptop: [],
iPad: [],
"Document Camera": [],
"Overhead Projector": [],
Chromebook: [],
Desktop: [],
MacBook: [],
Scanner: [],
});
// ======== Props =========== //
const props = defineProps({
propFormData: {},
});
// Now let's use Stein to retrieve the SS data
// eslint-disable-next-line no-unused-vars
const fetchSheetsData = function () {
const store = new SteinStore(
"https://api.steinhq.com/v1/storages/618e81028d29ba2379044caa"
);
store
.read("HS - Classrooms")
.then((data) => {
scrapDataHSClassrooms.value = data;
emptyRowsRemoved.value.forEach((item) => {
// Let's construct an object that separates equipment by type
// Check if property exists on oEquiptByType object
const exists = Object.prototype.hasOwnProperty.call(
oEquiptByType,
item["Equipment"]
);
// If item(row) is good lets push the row onto the corresponding Object Array
// in oEquiptByType. This will construct an object where each object property corresponds
// to an equipment category. And each oEquiptByType entry is an array where each array
// element is a row from the SS. e.g., oEquiptByType["Laptop"][3] is a row from
// SS and is a laptop.
if (exists) {
oEquiptByType[item["Equipment"]].push(item);
}
});
})
.catch((e) => {
console.error(e);
failure.value = true;
});
};
// =============== Called on component mount =============================== //
onMounted(fetchSheetsData);
The initial render is fine. Now I have a watcher on the prop so when someone submits a new item for the inventory I push that data onto the corresponding object array (ie, a laptop would be pushed onto the oEquiptByType[props.propFormData.Equipment] via oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
// ================================================================ //
// ======================= Watch effects ========================== //
// ================================================================ //
watch(props.propFormData, () => {
// Push the submitted form item onto the reactive
// oEquiptByType object array. This update of Vue state
// will then be injected into DOM and automagically update browser display.
oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
});
This works fine for the first item I add to backend as you can see here with original and then adding first item :
and after first item added (a laptop)
Notice the oEquiptByType[props.propFormData.Equipment] has the new item added. Great.
But now when I add a second item (a MacBook) is added this is resulting state:
Notice the Macbook array has been updated but also the Laptop array's last item has been overwritten with the Mac book entry??? And this behavior continues for any additional items added from a user. I have read docs over and do not see anything that would explain this behavior. I'm hoping maybe someone with more than my limited experience with Vue can help me out. Any additional info needed please let me know. Thanks...
Update:
Put a JSON.Stringify in watch function
Update two:
here is lineage of prop.FormData-
we start in form-modal and emit the form data like:
emit("emiterUIUpdate", formAsPlainObject);
then catch the data in the parent App.vue:
<FormModal
v-show="isModalVisible"
#close="closeModal"
#emiterUIUpdate="updateUI"
>
<DisplayScrap :propFormData="formData" />
const formData = reactive({});
// Method to be called when there is an emiterUIUpdate event emiited
// from form-modal.vue #param(data) is the form data sent from the
// form submission via the event bus. We will then send this data back
// down to child display-scrap component via a prop.
const updateUI = (data) => {
Object.assign(formData, data);
};
and then as posted previous in display-scrap.vue the prop propFormData is defined and watched for in the watch function. hope that helps..
It seems like the watch is getting triggered more often than you expect.
Might be that changes to props.propFormData are atomic and every incremental change triggers changes to the props, which in turn triggers the watch.
Try console logging the value of props.propFormData with JSON.stringify to see what changes are triggering it.
What happens here:
Your form modal emits the emiterUIUpdate event on Ok or Save (button)
Parent takes the object emitted and use Object.assing to copy all properties of emitted object to a formData reactive object. Instead of creating completely new object, you are just replacing the values of all properties of that object all and over again
The formData object is passed by a prop to child component and whenever it changes, it is pushed to target array
As a result, you have a multiple references to same object (formData hold by a parent component) and all those references are to same object in memory. Every Object.assign will overwrite properties of this object and all references will reflect those changes (because all references are pointing to the same object in memory)
Note that this has nothing to do with Vue reactivity - this is simple JavaScript - value vs reference
There is no clear answer to what to do. There are multiple options:
Simplest (and not clearest)
just do not use Object.assign - create new object every time "Save" is clicked
change formData to a ref - const formData = ref({})
replace the value of that ref on emiterUIUpdate event - formData.value = { ...data }
your watch handler in the child will stop working because you are watching props in a wrong way - instead of watch(props.propFormData, () => { use watch(() => props.propFormData, () => {
Better solution
the data should be owned by parent component
when modal emits new data (Save), Parent will just add the newly generated object into a list
share the data with DisplayScraps component using a prop (this can be a simple list or a computed creating object similar to oEquiptByType)
Let's say that two users do changes to the same document while offline, but in different sections of the document. If user 2 goes back online after user 1, will the changes made by user 1 be lost?
In my database, each row contains a JS object, and one property of this object is an array. This array is bound to a series of check-boxes on the interface. What I would like is that if two users do changes to those check-boxes, the latest change is kept for each check-box individually, based on the time the when the change was made, not the time when the syncing occurred. Is GroundDB the appropriate tool to achieve this? Is there any mean to add an event handler in which I can add some logic that would be triggered when syncing occurs, and that would take care of the merging ?
The short answer is "yes" none of the ground db versions have conflict resolution since the logic is custom depending on the behaviour of conflict resolution eg. if you want to automate or involve the user.
The old Ground DB simply relied on Meteor's conflict resolution (latest data to the server wins) I'm guessing you can see some issues with that depending on the order of when which client comes online.
Ground db II doesn't have method resume it's more or less just a way to cache data offline. It's observing on an observable source.
I guess you could create a middleware observer for GDB II - one that checks the local data before doing the update and update the client or/and call the server to update the server data. This way you would have a way to handle conflicts.
I think to remember writing some code that supported "deletedAt"/"updatedAt" for some types of conflict handling, but again a conflict handler should be custom for the most part. (opening the door for reusable conflict handlers might be useful)
Especially knowing when data is removed can be tricky if you don't "soft" delete via something like using a "deletedAt" entity.
The "rc" branch is currently grounddb-caching-2016 version "2.0.0-rc.4",
I was thinking about something like:
(mind it's not tested, written directly in SO)
// Create the grounded collection
foo = new Ground.Collection('test');
// Make it observe a source (it's aware of createdAt/updatedAt and
// removedAt entities)
foo.observeSource(bar.find());
bar.find() returns a cursor with a function observe our middleware should do the same. Let's create a createMiddleWare helper for it:
function createMiddleWare(source, middleware) {
const cursor = (typeof (source||{}).observe === 'function') ? source : source.find();
return {
observe: function(observerHandle) {
const sourceObserverHandle = cursor.observe({
added: doc => {
middleware.added.call(observerHandle, doc);
},
updated: (doc, oldDoc) => {
middleware.updated.call(observerHandle, doc, oldDoc);
},
removed: doc => {
middleware.removed.call(observerHandle, doc);
},
});
// Return stop handle
return sourceObserverHandle;
}
};
}
Usage:
foo = new Ground.Collection('test');
foo.observeSource(createMiddleware(bar.find(), {
added: function(doc) {
// just pass it through
this.added(doc);
},
updated: function(doc, oldDoc) {
const fooDoc = foo.findOne(doc._id);
// Example of a simple conflict handler:
if (fooDoc && doc.updatedAt < fooDoc.updatedAt) {
// Seems like the foo doc is newer? lets update the server...
// (we'll just use the regular bar, since thats the meteor
// collection and foo is the grounded data
bar.update(doc._id, fooDoc);
} else {
// pass through
this.updated(doc, oldDoc);
}
},
removed: function(doc) {
// again just pass through for now
this.removed(doc);
}
}));
This Meteor client public method needs to re run when the Meteor.user().profile.propA changes which is does fine, but it also runs when profile.propB changes or added. How can I stop it from re running when any other child property of profile has changed or added but only for profile.propA? Thanks
myListener: () => {
Tracker.autorun(() => {
if (Meteor.userId()) {
const indexes = Meteor.user().profile.propA;
if (!indexes || indexes.length <= 0) return;
dict.set('myStuff', indexes);
console.log('auto has run');
}
});
},
on the mongodb terminal:
db.users.update({'_id':'123abc'}, {$set: {'profile.propB':'B'}})
triggers the autorun. even though the reactive data source is Meteor.user().profile.propA;
Mongo.Collection.findOne allows you to specify which fields are retrieved from the local database using the fields option. Only changes to the fields specified there will trigger the autorun again.
Since Meteor.user() is just a shorthand for Meteor.users.findOne(Meteor.userId()), you can do the following to get updates for propA only:
const indexes = Meteor.users.findOne(Meteor.userId(), {
fields: {
'profile.propA': 1
}
});
Note that indexes will only contain profile.propA and the document's _id. If you need more data from the user document but still want to receive reactive updates separately, you have to fetch that data in a second autorun.
Is there a way to update a part of the URL reactively without using FlowRouter.go() while using React and react-layout?
I want to change the value in the document that is used to get the document from the DB. For example, if I have a route like ~/users/:username and update the username field in the document, I then have to user FlowRouter.go('profile', {data}) to direct the user to that new URL. The "old" route is gone.
Below is the working version I have, but there are two issues:
I have to use FlowRouter.go(), which is actually a full page refresh (and going back would be a 404).
I still get errors in the console because for a brief moment the reactive data for the component is actually wrong.
Relevant parts of the component are like this:
...
mixins: [ReactMeteorData],
getMeteorData() {
let data = {};
let users = Meteor.subscribe('user', {this.props.username});
if (user.ready())
data.user = user;
return data;
}
...
updateName(username) {
Users.update({_id:this.data.user._id}, {$set:{username}}, null, (e,r) => {
if (!e)
FlowRouter.go('profile', {username});
});
},
...
The route is like this:
FlowRouter.route('/users/:username', {
name: 'profile',
action(params) {
ReactLayout.render(Main, {content: <UserProfile {...params} />});
}
});
The errors I get in the console are:
Exception from Tracker recompute function:
and
TypeError: Cannot read property '_id' of undefined
Meteor Collections have a transform ability that allows behavior to be attached to the objects returned from mongo.
We want to have autopublish turned off so the client does not have access to the database collections, but we still want the transform functionality.
We are sending data to the client with a more explicit Meteor.publish/Meteor.subscribe or the RPC mechanism ( Meteor.call()/Meteor.methods() )
How can we have the Meteor client automatically apply a transform like it will when retrieving data directly with the Meteor.Collection methods?
While you can't directly use transforms, there is a way to transform the result of a database query before publishing it. This is what the "publish the current size of a collection" example describes here.
It took me a while to figure out a really simple application of that, so maybe my code will help you, too:
Meteor.publish("publicationsWithHTML", function (data) {
var self = this;
Publications
.find()
.forEach(function(entry) {
addSomeHTML(entry); // this function changes the content of entry
self.added("publications", entry._id, entry);
});
self.ready();
});
On the client you subscribe to this:
Meteor.subscribe("publicationsWithHTML");
But your model still need to create a collection (on both sides) that is called 'publications':
Publications = new Meteor.Collection('publications');
Mind you, this is not a very good example, as it doesn't maintain the reactivity. But I found the count example a bit confusing at first, so maybe you'll find it helpful.
(Meteor 0.7.0.1) - meteor does allow behavior to be attached to the objects returned via the pub/sub.
This is from a pull request I submitted to the meteor project.
Todos = new Meteor.Collection('todos', {
// transform allows behavior to be attached to the objects returned via the pub/sub communication.
transform : function(todo) {
todo.update = function(change) {
Meteor.call('Todos_update', this._id, change);
},
todo.remove = function() {
Meteor.call('Todos_remove', this._id);
}
return todo;
}
});
todosHandle = Meteor.subscribe('todos');
Any objects returned via the 'todos' topic will have the update() and the remove() function - which is exactly what I want: I now attach behavior to the returned data.
Try:
let transformTodo = (fields) => {
fields._pubType = 'todos';
return fields;
};
Meteor.publish('todos', function() {
let subHandle = Todos
.find()
.observeChanges({
added: (id, fields) => {
fields = transformTodo(fields);
this.added('todos', id, fields);
},
changed: (id, fields) => {
fields = transformTodo(fields);
this.changed('todos', id, fields);
},
removed: (id) => {
this.removed('todos', id);
}
});
this.ready();
this.onStop(() => {
subHandle.stop();
});
});
Currently, you can't apply transforms on the server to published collections. See this question for more details. That leaves you with either transforming the data on the client, or using a meteor method. In a method, you can have the server do whatever you want to the data.
In one of my projects, we perform our most expensive query (it joins several collections, denormalizes the documents, and trims unnecessary fields) via a method call. It isn't reactive, but it greatly simplifies our code because all of the transformation happens on the server.
To extend #Christian Fritz answer, with Reactive Solution using peerlibrary:reactive-publish
Meteor.publish("todos", function() {
const self = this;
return this.autorun(function(computation) {
// Loop over each document in collection
todo.find().forEach(function(entry) {
// Add function to transform / modify each document here
self.added("todos", entry._id, entry);
});
});
});