ridermansb
ridermansb

Reputation: 11059

Decoupled components with centralized states in Vuex + vue-router

The problem with share states is that is difficult to reuse actions and mutations in differents components.

Lets imagine that we we have a component Votes. This component allow users to vote on item

const Votes = {
  template: `<span>
        <i>{{ item.votes }}</i> <a href="#" @click.prevent="upvote">+</a>
    </span>
    `,
   methods: {
     upvote: function() {
       this.$store.dispatch('upvote', this.item.id)
     }
   },
   props: ['item']
}

So when user click on +, a action upvote is dispatch.

But how to reuse this component in two views, a list that list all items, and a details that display details about the item.

In both cases, we allow users to vote on item.

Details view List view

[ADDED] Vue-router

Users can navigate via URL, E.g. /item/a

In this case, should use router params to find item in database.

The store.items are empty!


The problem begins on store..

state: { items: [], opened: {} },
  actions: {
    open: function({commit, state}, payload) {
        let it = db.find(item => payload === item.id) // Find in db because user can navigate via Copy/Paste URL
      commit('SET_OPENED', it)
    },
    upvote: function({commit, state}, payload) {
        let it = state.items.find(item => payload === item.id) // Problem here, state.items is when i vote in ListingView, in ItemView (our details view) should use state.opened
      commit('SET_VOTE', { id: it.id, votes: it.votes + 1 })
    }
  },
  mutations: {
    SET_VOTE: function(state, payload) {
            let it = state.items.find(item => payload.id === item.id) // Problem here, state.items is when i vote in ListingView, in ItemView (our details view) should use state.opened
      console.log('Voted', db, it)
      Vue.set(it, 'votes', payload.votes)
    },
    SET_OPENED: function(state, payload) {
        Vue.set(state, 'opened', payload)
    }
  }

upvote and SET_VOTE are action and mutations that are called from diferents points (differents views), so the state is diferente.

Question

How to reuse same actions/mutations in differents views with differents states?

[Added] Remember

  1. User can navigate via URL, e.g /item/a and should display item
  2. The objetive is reuse actions/mutations and components. So duplicate all will no resolve this issue.

Full source....

const db = [{
  id: 'a',
  name: 'Item #1',
  image: 'http://lorempicsum.com/simpsons/350/200/1',
  votes: 0
}, {
  id: 'b',
  name: 'Item #2',
  image: 'http://lorempicsum.com/simpsons/350/200/2',
  votes: 0
}, {
  id: 'c',
  name: 'Item #3',
  image: 'http://lorempicsum.com/simpsons/350/200/3',
  votes: 0
}]

const Votes = {
  name: 'Votes',
  template: `<span>
	  	<i>{{ item.votes }}</i> <a href="#" @click.prevent="upvote">+</a>
    </span>
	`,
  methods: {
    upvote: function() {
      this.$store.dispatch('upvote', this.item.id)
    }
  },
  props: ['item']
}

const ListingView = {
  name: 'ListingView',
  template: `
    <ul class="listing">
    	<li v-for="item in $store.state.items">
				<router-link :to="{ name: 'item', params: { id: item.id }}">
      		<img :src="item.image" />
	  	    <br>{{ item.name }}	      
	      </router-link>
      	Votes: <votes :item=item></votes> 
    	</li>
		</ul>
  `,
  created() {
    this.$store.dispatch('fetch')
  },
  components: {
    Votes
  }
}

const ItemView = {
  name: 'ItemView',
  template: `<div class="item-view">
  		<router-link class="back-listing" :to="{name: 'listing'}">Back to listing</router-link>
	  	<div class="item">
  	  	<h1>{{ item.name }} <votes :item=item></votes> </h1>
    		<img :src="item.image" />
	    </div>
		</div>
  </div>`,
  computed: {
    item: function() {
      return this.$store.state.opened
    }
  },
  created() {
    this.$store.dispatch('open', this.$route.params.id) // I need this because user can navigate via Copy/Paste URL
  },
  components: {
    Votes
  }
}

const store = new Vuex.Store({
  state: {
    items: [],
    opened: {}
  },
  actions: {
    fetch: function({
      commit, state
    }, payload) {
      commit('SET_LIST', db.map(a => Object.assign({}, a))) // Just clone the array
    },
    open: function({
      commit, state
    }, payload) {
      let it = db.find(item => payload === item.id) // Find in db because user can navigate via Copy/Paste URL
      commit('SET_OPENED', it)
    },
    upvote: function({
      commit, state
    }, payload) {
      let it = state.items.find(item => payload === item.id) // Problem here, state.items is when i vote in ListingView, in ItemView should use state.opened
      commit('SET_VOTE', {
        id: it.id,
        votes: it.votes + 1
      })
    }
  },
  mutations: {
    SET_VOTE: function(state, payload) {
      let it = state.items.find(item => payload.id === item.id) // Problem here, state.items is when i vote in ListingView, in ItemView should use state.opened
      console.log('Voted', db, it)
      Vue.set(it, 'votes', payload.votes)
    },
    SET_OPENED: function(state, payload) {
      Vue.set(state, 'opened', payload)
    },
    SET_LIST: function(state, payload) {
      Vue.set(state, 'items', payload)
    }
  }
})
const router = new VueRouter({
  routes: [{
    name: 'listing',
    path: '/',
    component: ListingView
  }, {
    name: 'item',
    path: '/item/:id',
    component: ItemView
  }]
})
new Vue({
  el: '#app',
  store,
  router
})
* {
  box-sizing: border-box;
}
.listing {
  list-style-type: none;
  overflow: hidden;
  padding: 0;
}
.listing li {
  float: left;
  width: 175px;
  text-align: center;
  border: 1px #ddd solid;
  background: white;
  margin: 5px;
  cursor: pointer;
}
.listing li img {
  width: 100%;
  margin-bottom: 4px;
}
.listing li > a:hover {
  background: #eee;
}
.item-view {
  text-align: center;
}
.item {
  padding: 10px;
}
a {
  font-size: 16px;
  display: inline-block;
  padding: 10px;
  border: 1px #ddd solid;
  background: white;
  color: black;
  margin: 10px;
  &.back-listing {
    position: absolute;
    left: 0;
    top: 0;
  }
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
  <router-view></router-view>
</div>

Or in fiddle: http://jsfiddle.net/Ridermansb/sqmofcbo/3/

Was add another post (cross-posting) in Vue Forum

Upvotes: 4

Views: 978

Answers (1)

jostrander
jostrander

Reputation: 754

Just a a brief glance at your code, your issue is primarily that you are copying your current item into state.opened. Instead of doing that you should store a reference of the id of the currently opened item in state.opened and use that id to modify state.items.

Working example with a few extra comments on fixing it.

http://jsfiddle.net/d30o31r8/

Upvotes: 2

Related Questions