vuejs / pinia

🍍 Intuitive, type safe, light and flexible Store for Vue using the composition api with DevTools support
https://pinia.vuejs.org
MIT License
13.18k stars 1.06k forks source link

Since mutation is gone, we have no proper way to sync and track state across browser/node.js process #2105

Closed mannok closed 1 year ago

mannok commented 1 year ago

What problem is this solving

In the past with Vuex, we could sync store state across tabs or even node.js process by overriding mutation and propagate them. reference: vuex-electron The synchronization can be done well because mutations are cheap and synchronize. Also, by propagating mutations execution we can ensure the exact delta change among all the tabs/processes are the same.

However, as Pinia doesn't come with mutations, it even allow developer update state directly, we no longer be able to grab the exact delta change by mutation just like how we did before. Therefore, many pinia libraries that claimed to support Pinia state sync will cause unexpect side effect and watcher to be triggered under the hood, take pinia-shared-state as example:

  watch(
    () => store[key],
    (state) => {
      if (!externalUpdate) {
        timestamp = Date.now();
        channel.postMessage({
          timestamp,
          state: devalue.parse(devalue.stringify(state)),
        });
      }
      externalUpdate = false;
    },
    { deep: true },
  );

The mechanism under the hood is to deep tracking every root level state entry by DEEP watching them, which mean once any of the deep/nested property has changed, the syncing will be triggered. By reading the snippet above, we can see that the librar will propagate the whole state to other tabs, which means that all watchers [by object reference] from other tabs will be triggered, since a whole new state object is constructed and applied into these tabs state. To demostrate it as example, I provide the snippets below

// works fine
watch(
  () => store.level1.valueTypeProperty,
  () => { console.log('this watcher can always trigger fine as it is watching value type'); }
);

// will be trigger unexpectedly
watch(
  () => store.level1.referenceTypeProperty,
  () => { console.log('this watcher could trigger unexpectedly when store.level1.referenceTypeProperty.someValue changed'); }
);

Since we cannot always ask the developers to watch only value type, it must cause many watchers to be triggered and unexpected side effect will be occurred in long term.

Proposed solution

I keep thinking for few days and could not come out with any solution/workaround. I know the design is Pinia core basis and it could not be changed easily. Although I have some possible workaround in my head, it is not decent. Therefore, it would be nice if anyone can provide any workaround to achieve that. Thanks in advance

Possible Workaround:

  1. Find some way to optionally enforce developer to use only store.$patch(() => { /* update state */ }) to update state, the store.$patch() function returns the key path which the properties have been updated
  2. Bring mutation back
  3. Allow developer to enable DebuggerEvent for production so that we can have better trace on state mutation in store.$subscribe()

Describe alternatives you've considered

No response

posva commented 1 year ago

You could indeed enforce a team to use only $patch() or mutate within actions only, this can be added with an eslint plugin (as mentioned in other issues). The DebuggerEvent cannot be made available in production (see reason in Vue core docs).

You could find different ways of syncing state across processes, for example, you can send the whole state (which could be heavy). You can also create your own diffing algorithm to sync processes but this is outside of pinia's scope