Vue.js

The Practical Guide

@N_Tepluhina

 What will we learn today? 

- Shared state: when it becomes necessary

- Vuex

- Provide/inject

- A bit of Apollo (optional)

@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

new Vue({
  render: (h) => h(App),
  router,
  provide: {
    baseUrl: 'https://rickandmortyapi.com/api/',
  },
}).$mount('#app');

@N_Tepluhina

 Sharing non-reactive piece of state

// Episodes.vue

export default {
  inject: ['baseUrl'],
  created() {
    axios
      .get(this.baseUrl + '/episode')
	  ...
  },
};

@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
## OR
yarn add vuex
// main.js
import Vuex from 'vuex'

Vue.use(Vuex)

@N_Tepluhina

Manually

// main.js

const store = new Vuex.Store({
  state: {
    characters: []
  },
})

@N_Tepluhina

Accessing state in the component

// Characters.vue

computed: {
  characters() {
    return this.$store.state.characters;
  },
},

@N_Tepluhina

Better: mapping the state

// Characters.vue

import axios from 'axios';

export default {
  computed: {
    ...mapState(['characters']),
  },
}

@N_Tepluhina

Never ever change the state outside the mutation!

@N_Tepluhina

Mutations

  state: {
    characters: [],
  },
  mutations: {
    fetchCharacters(state) {
      state.characters = [
        {
          name: 'Test character',
          image:
            'https://upload.wikimedia.org/wikipedia/commons/a/a6/Anonymous_emblem.svg',
          location: {
            name: 'Unknown',
          },
        },
      ];
    },
  },

@N_Tepluhina

Running a mutation from the component (not recommended)

// Characters.vue

  created() {
    this.$store.commit('fetchCharacters');
  }

@N_Tepluhina

Constants for naming

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

@N_Tepluhina

Mutation types (optional)

// mutationTypes.js

export const FETCH_CHARACTERS = 'FETCH_CHARACTERS';
// store/index.js

import * as types from './mutationTypes';

export default new Vuex.Store({
  state: {
    characters: [],
  },
  mutations: {
    [types.FETCH_CHARACTERS](state) {
		...
    },
  },
});

@N_Tepluhina

Actions

// store/index.js

import * as types from './mutationTypes';

export default new Vuex.Store({
  state: {
    ...
  },
  mutations: {
	...
  },
  actions: {
    fetchCharacters({ commit }) {
      axios.get('/character').then((res) => {
        commit(types.FETCH_CHARACTERS, res.data.results);
      });
    },
  },
});

@N_Tepluhina

Actions

// App.vue

created() {
  this.$store.dispatch('fetchCharacters');
},

@N_Tepluhina

// App.vue

import { mapActions } from 'vuex';

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

Mapped actions

@N_Tepluhina

Fine-grained mutations

// store/index.js

mutations: {
  [types.REQUEST_CHARACTERS](state) {
    state.isLoading = true;
    state.error = null;
  },
  [types.RECEIVE_CHARACTERS_SUCCESS](state, payload) {
    state.isLoading = false;
    state.characters = payload;
    state.error = null;
  },
  [types.RECEIVE_CHARACTERS_SUCCESS](state, payload) {
    state.isLoading = false;
    state.error = 'Oh no! Something wrong happened!';
  },
},

@N_Tepluhina

Fine-grained mutations

// store/index.js

actions: {
  fetchCharacters({ commit }) {
    commit(types.REQUEST_CHARACTERS);
    axios
      .get('/character')
      .then((res) => {
      commit(types.RECEIVE_CHARACTERS_SUCCESS, res.data.results);
    })
      .catch(() => {
      commit(types.RECEIVE_CHARACTERS_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: {
  isCharacterInFavorites: (state) => (id) => {
    return state.favoriteCharacters.some((char) => char.id === id);
  },
},

@N_Tepluhina

Do not overuse getters!

// store/index.js

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

@N_Tepluhina

Modules

// store/index.js

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

export default new Vuex.Store({
  modules: {
    char,
  },
  ...
})
                            

@N_Tepluhina

Modules

// Characters.vue

computed: {
  ...mapState('char', {
    characters: (state) => state.characters,
    isLoading: (state) => state.isLoading,
  }),
    ...mapGetters(['isCharacterInFavorites']),
},               

@N_Tepluhina

Dynamic module registration

// store/index.js

const store = new Vuex.Store({ /* options */ })

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

@N_Tepluhina

Router + Vuex

// main.js

const store = new Vuex.Store({ ... })

const router = new VueRouter({
  mode: 'history',
  routes,
});

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

@N_Tepluhina

Router + Vuex

// main.js

const store = new Vuex.Store({ ... })

const router = new VueRouter({
  mode: 'history',
  routes,
});

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

@N_Tepluhina

Apollo Client freestyle! (if we have time for it)

@N_Tepluhina

Practice!

- Clone an exercise repository (same as yesterday)

- Move Episodes data to Vuex store

- Refactor Favorites logic to be a module

Q & A

@N_Tepluhina

Vue.js: The Practical Guide

By Natalia Tepluhina

Vue.js: The Practical Guide

Smashing Workshop - Day 4

  • 94

More from Natalia Tepluhina