Flicker from optimistic UI and partially simulated methods

Consider the following method:

  update(id, changes) {
    changes.updated = new Date;
    Messages.update({_id: id}, {$set: changes});

When I call e.g. Meteor.call('update', id, {format: 'markdown'}) on the client and observe the message in React with useTracker(() => Message.findOne(id)), I observe the following flickering sequence:

  1. The format changes to the intended value markdown.
  2. The format reverts to the original value.
  3. The format changes to the intended value markdown.

I surmise that the following optimistic UI behavior is happening:

  • Minimongo detects that the client’s and server’s changes are different, because they obtain different values for new Date.
  • Minimongo therefore unrolls the client’s changes, and then applies the server’s changes.

What seems broken to me is that Minimongo updates the client after unrolling the client’s changes but before applying the server’s changes. Minimongo has pauseObservers and resumeObservers to fix exactly this kind of flickering, but if I’m reading the source code right, this is only done when receiving updates from the server but not when rolling back expired updates from the client (?). Is this a bug, or intended behavior for a reason I’m missing?

One workaround I tried for these type of updates is performing changes.updated = new Date only if (Meteor.isServer). But the updates still seem to clash, so I get the same flickering behavior. Can you think of another workaround for setting reliable (server-defined) “last updated” dates?

I feel like this is a fundamental issue, but I couldn’t find a solution anywhere; apologies if I missed it.

1 Like

Just curios, your withTracker lives in a React Container?
Ok, in a React (functional) component, you have props and local state. Your rendered content should be dependent on the state. Your props should be managed within the useEffect hook.

You may see flickers because your page re-renders with no use. Sometimes pages re-render multiple times and because there are no changes the developer doesn’t see it (I mean visually in UI) but that affects performance/efficiency.

I’d say, beside what is going on in the pipeline of props, can you manage the rendering at the componendDidUpdate or useEffect level?

The React code could be as simple as this:

function Message({id}) {
  const message = useTracker(() => Message.findOne(id));
    {message ? message.format}

The issue is that message.format changes on the client from the old value to the new value, back to the old value, and then to the new value again. These are real changes, not null changes, so I don’t see how to avoid them… unless I add a slight delay with something like:

  [delayedMessage, setDelayedMessage] = useState()
  useEffect(() => {
    const t = setTimeout((() => setDelayedMessage(message)), 100);
    () => clearTimeout(t)
    {delayedMessage ? delayedMessage.format}

I would personally not use this structure. In React data has to stay in either a data container that wraps a component, in a global store like Redux or in a local state. You have to pass your data into a component via the component props or receive it in a component life cycle like ‘componentDidMount’ and push it into the local state. You link the local state into your view (HTML)

Honestly the behavior you describe here doesn’t seem to make sense to me. With latency compensation, if you apply the same changes on the client as are applied on the server (as should happen with client method simulation), then this exact scenario should be avoided. Only if a different result is applied by the server should the client side cache be invalidated and the UI change to reflect this.

Do you have a minimal reproduction that I could see to try to figure out why this might happen for you?

@copleykj I agree with you. But the changes on the client and server do differ, because of the new Date resolving to different times on client and server. I’d be happy with just the server setting this value, but this is still considered different from the client. I can put together a minimal repro.

@paulishca Isn’t the whole point of latency compensation / optimistic UI to show the user the local changes before they’ve hit the server? (My current solution is to turn off optimistic UI for this method, by not simulating it altogether. But I don’t think this is what Meteor was designed for…) Also, return values from hooks are the new notion of state.

1 Like

I am observing similar behavior without using new Date(). By modifying the database through .rawCollection().updateOne (inside a transaction) and at the same time simulating the modification on the client through .update: clients changes are applied, then rolled-back, then server changes are applied. I can see all states in the UI. Perhaps a timing issue? Sometimes it seems I only receive the final state (I deduct this from how many times and with what arguments React hooks get fired).

PS: Skipping the simulation as @edemaine suggested works around the problem in this particular case. But it would be much better to understand how to make optimistic UI behave correctly. Perhaps the solution lies in calling Meteor.refresh for all affected documents. But I am not ready to implement this yet. I’d rather design a generic/non-manual solution.