Ways of Vue components communication and How to use Vuex State Manager

2020年09月03日 977Browse 1Like 0Comments

Ways to share data between Vue components without middleware

  1. component props: parent --> child

  2. $parent, $child, ref and $refs: access parent, child or referred component data

  3. emit events: child (this.$emit('event_name', data)) ---> parent ( @event_name="eventHandler")

  4. $attrs/$listeners: pass comoponent props(v-bind) or events(v-on) downside level by level

  5. provide/inject: ancient component provides shared data for all the descendant components to use.

       // A.vue
       export default {
           provide: {
               sharedData: "value",  // provide shared data for descendent components
           }
       }
       

       // B.vue: A's descendant component
       export default {
           inject: ['sharedData'], // take shared data from ancient component
           mounted() {
           	console.log(this.sharedData);
       	}
       }
      

  6. Event Bus, $emit and $on: use an empty Vue instance as the global shared event bus. By means of events carried by the event bus, parent, child, sibling components can share data without restriction.

       // EventBus.js: Use it as a global shared bus/bridge to send/receive data by components
       import Vue from 'vue'
       export default const Event = new Vue();
       
       // Sender.js:  Sender component
       import { EventBus }  from './EventBus'
       EventBus.$emit('event_name', data);  // send data via specific event
       
       // Receiver.js: Receiver component
       import { EventBus }  from './EventBus'
       // get data passed by event namefrom Sender component
       EventBus.$on('event_name', data => { handle data; });
       
       // Remove event listener
       import { EventBus }  from './EventBus'
       // get data passed by event namefrom Sender component
       EventBus.$off('event_name', {});
       

    or add Event Bus to Vue prototype property in main.js

       import Vue from 'vue'
       Vue.prototype.$eventBus = new Vue();
       
       this.$eventBus.$emit('event_name', data);  // send data
       this.$eventBus.$on('event_name', data => { handle data; }); 	// receive data
       this.$eventBus.$off('event_name', {}); 	// remove event listener
       

  7. Web Storage API: HTML5 provides localStorage and sessionStorage API for data persistance for different scale and period. Thus, the page components can call these APIs to read/write data for sharing.

  8. slot-scope: Put slot-scope into template tag, and bind the template with data exposed by the child component. This allows the parent to render child component with different UI styles or data by the template slot. By this way, the parent component can used data inside the child component and render it with styles from parent itself. This is a very limited scope data sharing between parent and child components.

    	// parent component 
       <template>
         <div class="parent">
           <h3>This is the Parent Component</h3>
    	   
           <child>
             <template slot-scope="scope">
               <ul class="list-class"> <li v-for="item in scope.childData">{{item}}</li> </ul>
             </template>
           </child>
    	   
    	    <child>
             	<template slot-scope="scope">
               		<div class="div-class">
    					<span v-for="(item, index) in scope.childData">{{ index+1 }}: {{item}}</span> 
     				</div>
             	</template>
           </child>
         </div>
       </template>
       <style>
       .parent { ... }
       .list-class{ ... }
       .div-class{ ... }
       </style>
       

    	// child component
    	<template>
    	  <div class="child">
    		<h3>This is the Child Component</h3>
    		// child component exposes data for parent component
    		<slot :childData="data"></slot>
    	  </div>
    	</template>
    
    	 export default {
    		data: function(){
    		  return {
    			data: ['Alex','Brian','Jeremy','Teddy','Travis']
    		  }
    		}
    	}
    	

Why and When to use Vuex?

Ways to communicate between components mentioned above can be used according to different rscenarios. Some ways are strictly restricted by components relationship and are not flexible for global data sharing;

Athough the Event Bus provides a lightweight way for share data globally, it's not easy to manage events and state when it's used for large-scale application which has many components with complex relationship of data sharing, like: data may be shared from different levels of components, or multiple components, etc. The shared data mutation may cause issues, and code is not easy to maintain. Thus, a centralised state manager with data protection or other auxiliary features will provide more flexibility, maintenance to our development.

Vuex serves as a centralised store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. It also integrates with Vue's official devtools extension to provide advanced features such as zero-config time-travel debugging and state snapshot export / import.

Vuex Quick Start

npm install vuex

// store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);

// create store and export
export default new Vuex.Store({
	state: {},  	// add state content here
	mutations: {},  // add state mutation methods here
	actions: {},	// add async operation methods here: setTimeout(), axios.get(), ...
	getters: {},	// add computed methods(using state data to form new data and return) here
	modules: {},	//
});

// main.js
import store from "./store";

new Vue({
	store, // mount store to App root component
	render: (h) => h(App),
}).$mount("#app")

Vuex Core Concepts and How to Use

  • Store: the global object contains application state and operation methods.

  • State: ways to access(read) state in store. It is a single state tree - that is, this single object contains all your application level state and serves as the "single source of truth".

      const store = new Vuex.store({
          // signle state tree
          state: {
              count: 0
          },
      })
      

    2 ways to use state in store:

    	// way 1
    	this.$store.state.count
    
    	// way 2: call <code>mapState</code> function to map the state as component computed property
    	import { mapState } from 'vuex';  
    	computed: {
    		...mapState(['count']);  // use count as a computed property in this component
    	}
    	

  • Mutations: Do not use this.$store.state to modify the data directly(this is not the recommended way), call mutation method defined in store to mutate state. This lets store provide centralised management of state data, mutating state only by store's own methods.

      const store = new Vuex.store({
          state: {
              count: 0
          },
          // All the mutation methods are defined here
          mutations: {
              // define mutation methods
              addCount(state, payload) {
                  state.count += payload;
              }
          }
      })
      

    There are 2 ways to mutate state:

      // way 1: use this.$store.commit()
      this.$store.commit('addCount', payload); // submit data change in component
      
      // way 2: similarly to mapState(), mapMutations() will map the mutatioin method as a component method
      import { mapMutations } from 'vuex';  
      
      methods: {
          ...mapMutations(['addCount']);
          
          mutateHandler(){
              this.addCount(3); // call mapped mutation method
          }
      }
     

    Restriction: synchronised operations are not allowed here, like setTimeout(), IO requests, etc.

  • Actions: Use action methods defined in store to modify data asynchronously, but an action method must call mutation methods to update state.

      const store = new Vuex.store({
          state: {
              count: 0
          },
          // All the mutation methods are defined here
          mutations: {
              // define mutation methods
              addCount(state, payload) {
                  state.count += payload;
              }
          },
          // All the aysnc operations methods are defined here
          actions: {
              // define async mutation operation methods
              // call mutation methods only to update state
              asyncAddCount(context, payload) {
                  setTimeout(() => context.commit('addCount', payload), 1000);            
              }
          }
      })
      

    There are 2 ways to mutate state asynchronously:

      // way 1: use this.$store.dispatch()
      this.$store.dispatch('addCount', payload); // submit data change in component
      
      // way 2: similarly to mapState(), mapMutations() will map the mutatioin method as a component method
      import { mapActions } from 'vuex';  
      
      methods: {
          ...mapActions(['asyncAddCount']);
          
          actionHandler(){
              this.asyncAddCount(3); // call mapped action method
          }
      }
      

  • Getters Methods are used to wrap state in store and form new data to return.

    • Getters won't mutate the state in store, it just uses the data to generate new data.
    • The data returned by a gettter is reactive, that means when data(belongs to state) used by getter is changed in the store, the data returned by the getter will change as well.

      const store = new Vuex.store({
          state: {
              count: 0
          },
          // All the mutation methods are defined here
          mutations: {
              // define mutation methods
              addCount(state, payload) {
                  state.count += payload;
              }
          },
          // All the aysnc operations methods are defined here
          actions: {
              // define async mutation operation methods
              // call mutation methods only to update state
              asyncAddCount(context, payload) {
                  setTimeout(() =&gt; context.commit('addCount', payload), 1000);            
              }
          },
          //
          getters: {
              showCount(state) {
                  return <code>Latest count is ${state.count}</code>
              }
          }
      })
      

    Two ways to use getters:

      // way 1
      this.$store.getters.showCount
      
      // way 2: call mapGetters to map getters as a compute property of current component
      // then the getter can be used as a compute property
      import { mapGetters } from 'vuex';  
      
      computed: {
          ...mapGetters(['showCount']);  // showCount will be a compute property
      }
     

  • Modlues: Modules are used when the application state are too complex, we divide the store into categories by modules and manage them seprately in order to provide clearer logic and maintanable code. The following example is from official docs.

    const moduleA = {
      state: () =&gt; ({ ... }),
      mutations: { ... },
      actions: { ... },
      getters: { ... }
    }
    
    const moduleB = {
      state: () =&gt; ({ ... }),
      mutations: { ... },
      actions: { ... }
    }
    
    const store = new Vuex.Store({
      modules: {
        a: moduleA,
        b: moduleB
      }
    })
    
    store.state.a // -&gt; <code>moduleA</code>'s state
    store.state.b // -&gt; <code>moduleB</code>'s state
    

Reference

Sunflower

Stay hungry stay foolish

Comments