Search code examples
typescriptvue.jsvuexvuejs3

Vue re-render of one component breaks rendering of other


I have two components which display different information on the same data object. The object is placed in the store. Both components subscribe store actions to detect changes and re-render.

Component A is a child of Component B.

Component A

// used to track changes; refs.example is used to influence rendered DOM elements
const refs: {
  example?: Exercise,
} = reactive({
  example: undefined,
})

// subscribe to changes of selected
store.subscribeAction({
  after: (action) => {
    if (!subscribedActions.includes(action.type)) return
    // selected was altered, set example to trigger re-render
    refs.example = store.getters['selected'] as Example
  },
})

Component B

// Analog to component A

When selected is altered by an action, both refs.example are set and the components re-render. Component A re-renders before B.
Yet: Once B re-renders, Component A is rendered again as if its refs.example was undefined (if the setting of refs.example in Component B is delayed by setTimeout, Component A renders normally until Component B's refs.example is set). Though, if Component A is delayed until B has re-rendered, Component A never renders normally.

If B renders after A, there is a short a time when both components render normally. Component A is rendered "as undefined" at the end of this part of

Vue's source code

function flushJobs(seen) {
  isFlushPending = false;
  isFlushing = true;
  if ((process.env.NODE_ENV !== 'production')) {
      seen = seen || new Map();
  }
  flushPreFlushCbs(seen);
  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  queue.sort((a, b) => getId(a) - getId(b));
  // conditional usage of checkRecursiveUpdate must be determined out of
  // try ... catch block since Rollup by default de-optimizes treeshaking
  // inside try-catch. This can leave all warning code unshaked. Although
  // they would get eventually shaken by a minifier like terser, some minifiers
  // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
  const check = (process.env.NODE_ENV !== 'production')
      ? (job) => checkRecursiveUpdates(seen, job)
      : NOOP;
  try {
      for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
          const job = queue[flushIndex];
          if (job && job.active !== false) {
              if ((process.env.NODE_ENV !== 'production') && check(job)) {
                  continue;
              }
              // console.log(`running:`, job.id)

              // COMPONENT A IS RENDERED AS UNDEFINED HERE
              callWithErrorHandling(job, null, 14 /* SCHEDULER */);
          }
      }
  }
...

Any ideas on the issue and its solution?


Solution

  • While I do not know why this problem occured, I found a way to react to changes of a single object in store in two different components: Make use of store getters and computed():

    Component A

    const refs: {
      example?: Exercise,
    } = reactive({
      example: computed(() => store.getters['examples/selected'] as Exercise),
    })
    

    The same code could be used in Component B. Alternatively, since B is a child of A, A could pass the refs down to B.