Vue.js

The Practical Guide

@N_Tepluhina

 What will we learn today? 

- Shared state: when it becomes necessary

- Vuex

- Provide/inject

@N_Tepluhina

When do we need a shared state?

@N_Tepluhina

@N_Tepluhina

- Deep nested components to avoid prop drilling

- Router views

- Independent components

- Views with API requests

@N_Tepluhina

 Sharing non-reactive piece of state

- We don't add reactivity when it's not necessary

- Properties are available globally

- Great for injecting dependencies

@N_Tepluhina

 Sharing non-reactive piece of state

// main.js

const app = createApp({
  ...App,
  provide: { baseUrl: 'https://rickandmortyapi.com/api/' },
})

app.mount('#app')

@N_Tepluhina

 Sharing non-reactive piece of state

// Home.vue

export default {
  inject: ['baseUrl'],
  data() {
    return {
      ...
    }
  }
};

@N_Tepluhina

 Vuex

@N_Tepluhina

On project creation with Vue CLI

@N_Tepluhina

With plugin on CLI-based project

@N_Tepluhina

Manually

npm install vuex@next
## OR
yarn add vuex@next

@N_Tepluhina

Manually

// store/index.js

import { createStore } from 'vuex'

export default createStore({
  state() {
    return {
      items: [],
    }
  },
})

@N_Tepluhina

Manually

// main.js

import store from './store'

const app = createApp(App)
app.use(router)
app.use(store)

app.mount('#app')

@N_Tepluhina

Accessing state in the component

// Home.vue

computed: {
  searchResults() {
    return this.$store.state.items
  },
},

@N_Tepluhina

Better: mapping the state

// Home.vue

import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      searchResults: (state) => state.items,
    })
  }
}

@N_Tepluhina

Never ever change the state outside the mutation!

@N_Tepluhina

Mutations

export default createStore({
  state() {
    return {
      items: [],
    }
  },
  mutations: {
    fetchResults(state) {
      state.items = [
        {
          id: '1',
          name: 'Test',
          description: 'Test description',
          liked: false,
        },
      ]
    },
  },
})

@N_Tepluhina

Running a mutation from the component (not recommended)

// Home.vue

created() {
  this.$store.commit('fetchResults')
},

@N_Tepluhina

Constants for naming

  mutations: {
    FETCH_RESULTS(state) {
      ...
    },
  },

Mutation types (optional)

// store/mutationTypes.js

export const FETCH_RESULTS = 'FETCH_RESULTS'
// store/index.js

import * as types from './mutationTypes';

export default createStore({
  state() {
    return {
      items: [],
    }
  },
  mutations: {
    [types.FETCH_RESULTS](state) {
      /*...*/
    },
  },
})

@N_Tepluhina

Actions

// store/index.js

import * as types from './mutationTypes';

export default new Vuex.Store({
  state: {
    ...
  },
  mutations: {
	...
  },
  actions: {
    async fetchResults({ commit }, searchTerm) {
      // axios request to fetch the results

      commit(types.FETCH_RESULTS, results)
    },
  },
});

@N_Tepluhina

Actions

// Home.vue

created() {
  this.$store.dispatch('fetchResults', this.searchTerm)
},

@N_Tepluhina

// Home.vue

import { mapActions } from 'vuex';

export default {
  methods: {
    ...mapActions(['fetchResults']),
  },
  created() {
    this.fetchResults(this.searchTerm);
  },
};

Mapped actions

@N_Tepluhina

Fine-grained mutations

// store/index.js

mutations: {
  [types.REQUEST_RESULTS](state) {
    state.loading = true
    state.error = null
  },
  [types.RECEIVE_RESULTS_SUCCESS](state, payload) {
    state.loading = false
    state.items = payload
    state.error = false
  },
  [types.RECEIVE_RESULTS_ERROR](state) {
    state.loading = false
    state.items = []
    state.error = true
  },
},

@N_Tepluhina

Fine-grained mutations

// store/index.js

actions: {
  async fetchResults({ commit }, searchTerm) {
    const endpoint = searchTerm.length ? '/breeds/search' : '/breeds'
    commit(types.REQUEST_RESULTS)

    try {
	  /* Logic for fetching results */

      commit(types.RECEIVE_RESULTS_SUCCESS, results)
    } catch {
      commit(types.RECEIVE_RESULTS_ERROR)
    }
  },
},

@N_Tepluhina

Fine-grained mutations

fetch<Entity> action

REQUEST_<Entity> mutation

RECEIVE_<Entity>_SUCCESS mutation

RECEIVE_<Entity>_ERROR

mutation

@N_Tepluhina

Getters

// store/index.js

getters: {
  numberOfLiked(state) {
    return state.items.filter((item) => item.liked).length
  },
},

@N_Tepluhina

Do not overuse getters!

// store/index.js

getters: {
  getItems(state) {
    return state.items // This is bad, use mapState instead!
  }
},

@N_Tepluhina

Modules

// store/index.js

const items = {
  namespaced: true,
  state: () => ({...}),
  mutations: {...},
  actions: {...},
  getters: {...},
};

export default createStore({
  modules: {
    items,
  },
  ...
})
                            

@N_Tepluhina

Modules

// Characters.vue

computed: {
  ...mapState('items', {
    items: (state) => state.items,
    loading: (state) => state.loading,
  }),
},               

@N_Tepluhina

Dynamic module registration

// store/index.js

const store = createStore({ /* options */ })

store.registerModule('items', {
  // ...
})

@N_Tepluhina

Router + Vuex

// main.js

import store from './store'

const router = createRouter({
  routes,
  history: createWebHistory(),
})

router.beforeEach((to, from, next) => {
  if(!store.state.isAuthenticated) next({name: 'login'})
  else next()
})

Q & A

@N_Tepluhina