Vuex: Publisher/subscriber pattern in vuex

Created on 8 Mar 2018  路  12Comments  路  Source: vuejs/vuex

What problem does this feature solve?

To notify components in a long running task

While porting some code to a vuex module, I had to re-think the synchronisation pattern and while the reactive pattern is the obvious choice, I went back to pub/sub as described in https://github.com/vuejs/vuex/issues/673#issuecomment-371469565 and posted on https://forum.vuejs.org/t/solved-publisher-subscriber-pattern-in-vuex/29176.

The gist is to notify components in a long running task.

Short of implementing a streaming pattern, I think that evolving the subscribe API is an easier solution that solve the use-case efficiently.

Currently there exist two API calls for pub/sub. subscribe and subscribeAction. While subscribe listen to all mutations (making it inefficient), subscribeAction is pretty limiting. Instead of suggesting and implementing subscribeCommit, I suggest leveraging vue's emit/on system. The plugin system is subject to this exact same discussion.

By using vue's pub/sub system, we can remove subscribeAction and have a much more flexible system. I made a hack of this, posted in the vue forum, that works very well.

_Inside vuex action or commit:_

this._vm.$emit('event', payload)

_Inside a vue component_

this.$store._vm.$on('event', payload => {/* do something useful */})

Before doing a PR, I hope to discuss this with you.

I should note that, my suggestion works equally well in _classic_ mode and _module_ mode.

What does the proposed API look like?

store.subscribe('pages/loaded', (payload, state) => { /* do something useful */ })

Note that mutation is changed to payload because the type is now insignificant (the first argument to subscribe tells us what we're interested in).

Inside a mutation, getter or action:

this.nofity('event', { a: 'b', c: ['d'] })

Full list of API change:

  1. store.subscribe
  2. store.nofity
  3. store.unsubscribe
  4. store.subscribeOnce

Most helpful comment

Please keep this issue open while the discussion is ongoing. It's a bit rude to just close it.

All 12 comments

This API will break the Vuex's unidirectional data flow which is the core concept of Vuex. The users would able to mutate/read store state everywhere in Vuex. Then it would make the entire application data flow so complicated and unpredictable.

Also this functionality is not needed to be implemented in the Vuex since it is just an event emitter. I would suggest you to use some event emitter (probably empty Vue instance) in actions.

export const emitter = new Vue()

export default {
  // ...

  actions: {
    // should be called when the store is initialized
    // to observe events
    observe({ dispatch, commit }) {
      emitter.$on('some-event', () => {
        commit('someEvent')
      })

      emitter.$on('other-event', () => {
        dispatch('otherEvent')
      })
    },

    // notify some event in action
    notify({ state }) {
      emitter.$emit('notify', state.someValue)
    }
  }
}

FYI: you should not use it in your getters and mutations since they are meant to be only used for reading derived state and updating state.

@ktsn That is easy to remedy. Just don't send state to a listener.

In my use case I'm interested in one property which is changed in a commit. It makes sense to target the mutation commit and not an action, in this case.

The issue with a global emitter is apparent in an application structure such as nuxt. While it is possible, it deviates a lot from a normal nuxt project because the store setup is generated in the build process and is not something you do manually as in your example.

I was thinking about just passing the payload to listeners but then I just copied the current vuex implementation of subscribe. In fact with subscribe it is possible to _to mutate/read store state everywhere in Vuex_.

Please keep this issue open while the discussion is ongoing. It's a bit rude to just close it.

My suggestion is also already in the current design. Only hidden by an _underscore. E.g. this._vm.$emit.

Then could you describe your use case in more detail with some snippets that cannot be achieved by external event emitter? Looks like the proposal does not include the needs which must be in Vuex.

I would say exposing event emitter in the action might make sense if your use case must need it but it's never be in getters and mutations as I already described. Getters should be pure function to return derived state since it will cache the returned value and would cause unexpected behavior if you make any side effects in it. Mutations should only mutate a state to track its changes easily in the devtools, logger or etc.

Just don't send state to a listener.

I don't think that is a good idea. If we don't want the user to do some usage, we should design the API not to able to do that.

My suggestion is also already in the current design. Only hidden by an _underscore. E.g. this._vm.$emit.

You should not use that. It is not in the public api and may be changed in the future without announces.

I described my use case in much greater detail in the #673.

I have a slightly different use-case. I got a (http) loader running in a webworker which is sent batches of requests from a vuex action. Before using vuex, I had a pub/sub system where the loader would emit successful requests and errors. Since the fetching is ongoing and can be both successful and unsuccessful, promises are out since they can only resolve/reject once.

Successful requests are easy. Just commit the change and let vuex handle the reactive stuff. I have not found any documentation or text regarding failure state management. I will just go ahead and implement something but I would appreciate having a look at how other people are handling this. I guess, if I set a computed property to an empty error struct in vuex in any component that cares and then they will find out if the requests is their responsibility and do what-ever action required.

I agree with you about having this functionality in a getter. It doesn't belong there. My main aim is to have a finer grained control on what I'm listening for. You're also right that I can achieve my goal in the action that attach listeners to my fetcher. Note, that it's not a general purpose pre-fetcher, it only fetches images and SVG's from a converted PDF and does some work on the returned SVG before emitting the loaded event. It should also be remarked that the loader can be sent more batches before it has finish the current batch. It will simply queue them, unless a cancel call is made. In that case it will flush its queue.

Instead of just having to observe an array of objects, I would like to be able to specify something like "pages/pages/loaded" - e.i. listening only on mutations on the loaded property of objects in the pages array in the pages vuex module. A normal vue watcher would get the entire new array and old array, and you have to do the comparison yourself.
The second aim is to not call code on every mutation in the pages module. My naive attempt was to target a single commit mutation instead.
Thirdly, I would like an idiomatic implementation. I can think of nothing better than using the vuex API.

There might be a way to be reactive to single property mutations in a collection with vue/vuex but I'm unaware of it.

My current implementation listens to the loaded commit. You mention that you don't think notify should be available in mutations, but I fail to understand why? Below is my current implementation. I have removed state from the handler function, since using a getter is suppose to be better (I hope the caching understand when an underlying object has changed).

this.$store.subscribe(mutation => {
  if (mutation.type === 'pages/loaded') {
    const loadPage = mutation.payload
    const page = this.get(loadPage.id)

    if (loadPage.type === pageDisplayEnum.svg) {
      this.renderPage({ id: loadPage.id, pageDisplay: pageDisplayEnum.svg })
    } else if (loadPage.type === pageDisplayEnum.img && !page.renderSvg) {
      this.renderPage({ id: loadPage.id, pageDisplay: pageDisplayEnum.img })
    }
  }
})

The get getter is implemented as:

export const getters = {
  get (state) {
    return id => state.pages[id]
  }
}

I can, with the current API, bypass the getter by reading the state, send with the subscribe handler function.

```js
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'pages/loaded') {
const loadPage = mutation.payload
const page = state.pages.pages[loadPage.id]

if (loadPage.type === pageDisplayEnum.svg) {
  this.renderPage({ id: loadPage.id, pageDisplay: pageDisplayEnum.svg })
} else if (loadPage.type === pageDisplayEnum.img && !page.renderSvg) {
  this.renderPage({ id: loadPage.id, pageDisplay: pageDisplayEnum.img })
}

}
})
```

I'm getting at, why is it a problem with my API change suggestion, when it is already implemented like that?

Seems like this doesn't have any interest at the vuex team or other users, so I'm closing this.

It's a shame @dotnetCarpenter as this feature would've been ideal for me too.

In my use case, I need to trigger an update method on a chart with some new data when state changes... I don't need to access or modify the data (so don't need to pass it in the event), I just need to fire another method.

There are definitely other ways to achieve this - this one just 'felt' better.

@smarterdigitalltd I would also guess it would be more performant than any other alternative you will have to do.
Maybe it's worth to revisit later but for now, I have moved on and is busy with other things.

I was liking this. Shame they didn't.

I think it useful to allow subscribing to specific mutation.

Was this page helpful?
0 / 5 - 0 ratings