Vuex: Protocol/api for cross-window, even cross-process state management

Created on 10 Mar 2016  Â·  15Comments  Â·  Source: vuejs/vuex

i.e. Use Vuex in multi-window Electron applications.

discussion

Most helpful comment

I fiddled around a bit with this today in Electron and got something working. This is by no means a fully-baked solution, but should be useful for a discussion. The gist:

  • The background process holds the "main" Vuex.Store.
  • Each BrowserWindow gets a "client" Vuex.Store.
  • All stores have the same mutations and state shape.

I don't see any obvious way around the copies since inter-process messages are serialized.

Now for some code... The main store is created first, and the main process begins listening for client stores to connect.

// background.js
import { BrowserWindow, ipcMain } from 'electron'

import Vue from 'vue'
import Vuex from 'vuex'
import { mutations } from './store'

Vue.use(Vuex)

const clients = []

const store = new Vuex.Store({
  state: {
    messages: []
  },
  mutations,
  middlewares: [{
    onMutation (mutation, state) {
      Object.keys(clients).forEach((id) => {
        clients[id].send('vuex-apply-mutation', mutation)
      })
    }
  }]
})

ipcMain.on('vuex-connect', (event) => {
  let winId = BrowserWindow.fromWebContents(event.sender).id
  console.log('[background] vuex-connect', winId)

  clients[winId] = event.sender
  event.returnValue = store.state
})

ipcMain.on('vuex-mutation', (event, {type, payload}) => {
  console.log('[background] vuex-mutation ', type, payload)
  store.dispatch(type, ...payload)
})

Newly-created windows are initialized with a current copy of the "main" store's state. To make sure mutations are applied in a consistent order to all stores, a "client" cannot apply mutations to itself directly. It asks the "main" store to perform the mutation which is then replicated to all registered "client" stores (done by monkey-patching dispatch here).

// store.js
import Vuex from 'vuex'
import createLogger from 'vuex/logger'
import { ipcRenderer } from 'electron'

export const mutations = {
  RECEIVE_MESSAGE (state, message) {
    state.messages.push(message)
  }
}

export const middlewares = [
  createLogger()
]

export default function getClientStore (_Vue) {
  _Vue.use(Vuex)

  let store = new Vuex.Store({
    state: ipcRenderer.sendSync('vuex-connect'),
    mutations,
    middlewares
  })

  store._dispatch = store.dispatch

  store.dispatch = function (type, ...payload) {
    // Stolen from vuejs/vuex
    if (typeof type === 'object' && type.type && arguments.length === 1) {
      payload = [type.payload]
      type = type.type
    }

    console.log('[client] dispatching ', type, payload)
    ipcRenderer.send('vuex-mutation', { type, payload })
  }

  ipcRenderer.on('vuex-apply-mutation', (event, {type, payload}) => {
    console.log('[client] vuex-apply-mutation', type)
    store._dispatch(type, ...payload)
  })

  return store
}

And finally, include a "client" store in each of your windows' main Vue instance.

// window1.js, window2.js, etc...

import Vue from 'vue'
import getClientStore from './store'
import App from './App'

new Vue({
  store: getClientStore(Vue),
  el: 'body',
  components: { App }
})

And the end result is something like below. You can refresh any window individually, and it's store will always be created with the latest shared state.

multi-vuex

All 15 comments

I fiddled around a bit with this today in Electron and got something working. This is by no means a fully-baked solution, but should be useful for a discussion. The gist:

  • The background process holds the "main" Vuex.Store.
  • Each BrowserWindow gets a "client" Vuex.Store.
  • All stores have the same mutations and state shape.

I don't see any obvious way around the copies since inter-process messages are serialized.

Now for some code... The main store is created first, and the main process begins listening for client stores to connect.

// background.js
import { BrowserWindow, ipcMain } from 'electron'

import Vue from 'vue'
import Vuex from 'vuex'
import { mutations } from './store'

Vue.use(Vuex)

const clients = []

const store = new Vuex.Store({
  state: {
    messages: []
  },
  mutations,
  middlewares: [{
    onMutation (mutation, state) {
      Object.keys(clients).forEach((id) => {
        clients[id].send('vuex-apply-mutation', mutation)
      })
    }
  }]
})

ipcMain.on('vuex-connect', (event) => {
  let winId = BrowserWindow.fromWebContents(event.sender).id
  console.log('[background] vuex-connect', winId)

  clients[winId] = event.sender
  event.returnValue = store.state
})

ipcMain.on('vuex-mutation', (event, {type, payload}) => {
  console.log('[background] vuex-mutation ', type, payload)
  store.dispatch(type, ...payload)
})

Newly-created windows are initialized with a current copy of the "main" store's state. To make sure mutations are applied in a consistent order to all stores, a "client" cannot apply mutations to itself directly. It asks the "main" store to perform the mutation which is then replicated to all registered "client" stores (done by monkey-patching dispatch here).

// store.js
import Vuex from 'vuex'
import createLogger from 'vuex/logger'
import { ipcRenderer } from 'electron'

export const mutations = {
  RECEIVE_MESSAGE (state, message) {
    state.messages.push(message)
  }
}

export const middlewares = [
  createLogger()
]

export default function getClientStore (_Vue) {
  _Vue.use(Vuex)

  let store = new Vuex.Store({
    state: ipcRenderer.sendSync('vuex-connect'),
    mutations,
    middlewares
  })

  store._dispatch = store.dispatch

  store.dispatch = function (type, ...payload) {
    // Stolen from vuejs/vuex
    if (typeof type === 'object' && type.type && arguments.length === 1) {
      payload = [type.payload]
      type = type.type
    }

    console.log('[client] dispatching ', type, payload)
    ipcRenderer.send('vuex-mutation', { type, payload })
  }

  ipcRenderer.on('vuex-apply-mutation', (event, {type, payload}) => {
    console.log('[client] vuex-apply-mutation', type)
    store._dispatch(type, ...payload)
  })

  return store
}

And finally, include a "client" store in each of your windows' main Vue instance.

// window1.js, window2.js, etc...

import Vue from 'vue'
import getClientStore from './store'
import App from './App'

new Vue({
  store: getClientStore(Vue),
  el: 'body',
  components: { App }
})

And the end result is something like below. You can refresh any window individually, and it's store will always be created with the latest shared state.

multi-vuex

I did something very similar to achieve this. I think this is a great candidate for a reusable middleware solution for Vuex. Something like https://github.com/samiskin/redux-electron-store?

@altitudems Nice, hadn't seen that before. Having never even seen Redux code, I'm not entirely sure what I'm looking at, but it appears they're doing a bit more around syncing the actual state rather than just replicating mutations?

I still need to think through how to let each "client" store have shared state and "local" state--maybe a shared module or something. I'd also like to figure out how to do this without monkey-patching dispatch...

Yeah it would be nice to be able to intercept mutations. I do something like this...

store.subscribe((mutation)=>{
  if(!mutation.payload.remote){
    socket.emit('mutation', mutation)
  }
})

socket.on('mutation',function(mutation){
  mutation.payload.remote = true
  store.commit(mutation.type,mutation.payload)
})

This is how I prevent the remote mutations for being echoed back to the server. Is there another way to do this? It would be nice to have middleware functionality that will allow modification of a mutation before it's truly committed or even the ability to stop propagation by not calling next() like in Express. Any more ideas out there?

I ended up hijacking/wrapping store.commit() with the functionality I needed.

For those who reading this issue and using @bradstewart solution, you should do some update

in main

there's no middlewares now, you should use subscribe instead

if you have these code:

const store = new Vuex.Store({
  // ...
  middlewares: [{
    onMutation (mutation, state) {
      Object.keys(clients).forEach((id) => {
        clients[id].send('vuex-apply-mutation', mutation)
      })
    }
  }]
})

please remove middlewares and append below to next line

store.subscribe((mutation, state) => {
  Object.keys(clients).forEach((id) => {
    clients[id].send('vuex-apply-mutation', mutation)
  })
})

full example: https://github.com/LightouchDev/MasterVyrn/blob/master/src/main/libs/store.js

in renderer

use official function replaceState to import state.

if you have these code:

  let store = new Vuex.Store({
    state: ipcRenderer.sendSync('vuex-connect'),
    mutations,
    middlewares
  })

you can just use:

  let store = new Vuex.Store({
    state,
    mutations,
    middlewares
  })
  store.replaceState(ipcRenderer.sendSync('vuex-connect'))

if you use modules instead of state:

const store = new Vuex.Store({
  modules,
  strict: process.env.NODE_ENV !== 'production' // this up to you
})

// import master state
try {
  store.replaceState(ipcRenderer.sendSync('vuex-connect'))
  log('master state imported!')
} catch (error) {
  err('import master state failed: %s', error)
}
// log, err is generate from 'debug' node package

and don't forget to hijack/wrapping store.commit or store.dispatch for your use.

full example: https://github.com/LightouchDev/MasterVyrn/blob/master/src/renderer/store.js

thanks for sharing! I've added a close event listener on each window to prevent sending to a closed window.

ipcMain.on('vuex-connect', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  let winId = win.id
  console.log('[background] vuex-connect', winId)

  win.on('close', () => {
    clients[winId] = null
    delete clients[winId]
  })

  clients[winId] = event.sender
  event.returnValue = store.state
})

you notice me we are using BrowserWindow id instead of webContent id, why don't just save webContent id in clients array and listen on destroyed event?

ipcMain.on('vuex-connect', (event) => {
  const win = event.sender
  const winId = win.id
  log('[vuex] new vuex client: %s', winId)

  win.on('destroyed', () => {
    clients[winId] = null
    delete clients[winId]
  })

  clients[winId] = win
  event.returnValue = store.state
})

@miaulightouch sounds even better to me as this also get's around https://github.com/electron/electron/issues/10674

State management through ipc does not work with functions. I keep forgetting that and was wondering why some functions never reached the store. Just in case someone is trying to do the same. Otherwise this is a pretty neat idea.

@burningTyger can you give a quick example how you’re running into issues with functions?

read ipcRenderer document:

Arguments will be serialized in JSON internally and hence no functions or prototype chain will be included.

maybe you should define in actions, mutations

@joernroeder, yup, that's the reason why.

Hi guys,

I've created the special package for Vuex and Electron integration:
– https://github.com/vuex-electron/vuex-electron

We'll be closing this issue since this is a really old one. Also, as mentioned above, there's a plugin to do this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

taoeffect picture taoeffect  Â·  3Comments

Ge-yuan-jun picture Ge-yuan-jun  Â·  3Comments

jdittrich picture jdittrich  Â·  3Comments

niallobrien picture niallobrien  Â·  3Comments

gongzza picture gongzza  Â·  3Comments