Not returning a value wouldnât fix my problem. I have a weird edge-case situation with optimistic UI and shared user reactivity. Iâll explain - sorry for the length:
The Problem
I have a template SharedProductViewer
that multiple users in different logged in sessions can collaboratively use to review a Product
together. SharedProductViewer
gets updated with a new set of Product
data based on an index
passed into it. The index
is from an array of chosen grouped Products
to review. ALL the Products
are already loaded into minimongo on the client for SharedProductViewer
. This helps with UI quickness when switching products.
When this product switch happens, it needs to reset a bunch of user-enabled product view states (e.g. render type, layers, etc.) that are controlled by additional mongo fields
on a SharedProductViewer
document that powers the SharedProductViewer
template. The SharedProductViewer
template needs to reset basically, but in the design, it does not get destroyed or recreated when switching the index
, only ârehydratedâ with the new already loaded data (otherwise this wouldnât be a problem to reset the views).
Upon switching the index
, the SharedProductViewer
view state fields
are reset on the server in a Collection hook. This is how they get reset.
When doing a Meteor Method call to updateIndex
from the client, the index
, like all the viewer fields
, needs to be persisted to the database to drive reactivity from one user making an update and having it affect all other collaborative users looking at SharedProductViewer
. It will indeed update the index
and the SharedProductViewer
template updates immediately due to optimistic UI (as all the Products
are already loaded into minimongo). However, the reset views which are also tied to SharedProductViewer
database fields, get updated in a Collection hook when the index
gets updated. Basically the server cleaning up the view upon switching the Product
index as there are multiple âviewsâ open by various logged in users collaborating. The viewerâs state isnât tied to a user session, itâs tied to the database (a very Meteor use-case BTW).
The problem is the UI flickers when the next Product
is shown in the SharedProductViewer
template because it momentarily shows the enabled views because they donât get reset until the Collection hook resets them.
I have an autorun
that uses Template.currentData()
to update the viewer reactively. This is the order of what happens:
- Meteor method call to client/server
updateIndex
that updates the index
field only.
- This causes optimistic UI to trigger the
autorun
because it threads into Template.currentData()
. This makes SharedProductViewer
go to the next product with the unwanted views enabled before the server collection hook updates finish.
- The server Collection hooks run and reset all the views. This triggers the
autorun
with Template.currentData()
and resets the views.
Some Solutions
If there was a way to just call updateIndex
with optimistic UI disabled, this problem wouldnât happen as all the updates would come back from the server together. As an interesting note, this problem only happens when a user updates the index
from their client SharedProductViewer
. If the update comes from another user, it works exactly right because all the updates are coming from the server together - i.e. thereâs no local optimistic UI update to index
that flickers the view.
I could make updateIndex
server only, but all the Products
are loaded already in the client, so I want to take advantage of that and have the client update quickly. Iâd hate to lose optimistic UI and force everything to be server-only just to fix a UI flicker.
I tried an approach using ReactiveVar
to control the views enabled in SharedProductViewer
and just reset the views locally before the Meteor Method call to updateIndex
. Basically âcontrollingâ the UI myself manually on the client - and unhinging it from database fields. This doesnât work because due to reactivity and needing the SharedProductViewer
to allow reactive updates from the server from other users. I have to tie those ReactiveVars
to Template.currentData()
to achieve server reactivity. This then kills this approach as optimistic UI will overwrite my manual attempts control the UI. When it goes to set index
in Template.currentData()
it will also overwrite all the view fields
from the server that have not been updated yet.
Another approach is to simply add the view resets in the mongo updater in the client/server method updateIndex
. This then âaddsâ them from the client making them set correctly for optimistic UI and it works perfectly. But, I donât really like this approach because this exactly duplicates what the server does and adds extra unwanted mongo code. And the Collection hooks catches it for ALL the various collaborative users when a Product
index is updated.
Another approach is to just simply design it so it destroys the template and re-creates it. But this is also overkill and would cause overhead in the UI.
The solution I settled on builds on the above ReactiveVar
solution and simply adds another ReactiveVar
control boolean enableReactiveUpdates
in the template. Before updateIndex
is called, I disable enableReactiveUpdates
. This bypasses the Template.currentData()
call in the autorun
. Then in the success callback of updateIndex
I enable enableReactiveUpdates
in a Tracker.afterFlush()
.
This approach momentarily stops Template.currentData()
(as set by optimistic UI) from changing my manual resets to the UI. So the process looks like this now:
- Disable
enableReactiveUpdates
causing no optimistic UI because Template.currentData()
gets by passed in the autorun
.
- Reset all the
ReactiveVar
views and a ReactiveVar
index
that updates the UI instantly.
- Call
updateIndex
(which is just to update the database and all other collaborative viewers).
- In the
updateIndex
success callback, re-enable enableReactiveUpdates
. This triggers the autorun
and everything is synced up with the server.
This way still keeps the optimistic UI snappy, but doesnât let any delayed server field
values from flickering the UI.
The gotcha is you have to use Tracker.afterFlush()
in the success callback of updateIndex
or else it will re-enable Template.currentData()
too early. The flush makes sure everything from the updateIndex
method call is updated to the template. This approach does however basically make it a server-only method.
The ultimate problem to put it simply, is Iâm trying to control what happens on the client, when a client update causes server side-effects that have an effect on the UI. Weâre years baked in our design at this point, but I will redesign all this at some point.