Herd those API calls! Transitioning to VueX

M. Wallace

Or, “How not to build a web app”

You’ll know when you need them.”

A friend recently got glasses, and she was surprised. She didn’t know she needed them, but afterwards felt like the world was in HD.

I’ve lived a lot of my coding life like her. I didn’t know what I didn’t know.

With this Wordpress+Vue theme I’m building out, I’m taking the time that I often don’t afford myself with client/work/on-deadline projects.

Reinventing the wheel isn’t fun, and neither is refactoring 3 times in 2 weeks after an early-optimization. Good thing too.

That “Single Source of Truth” tho..

Turns out the single-data-source idea is moving target. There are 3 common ways to handle a web-app’s internal data:

  1. On-load (no AJAX),
  2. Direct-from-API (AJAX),
  3. API-via-Local Store (Cached & Managed).
Cheatsheet

I knew I wasn’t “doing it right” when I released code with the “Parallel, Self-Contained” model. But the data was staying on-page, and it wasn’t really hurting anything except load-times.

For a brief moment, I started to bootstrap some data on-load in the PHP-based index.php file. That was the really, really bad “Dual-Ended” approach which would destroy my sanity.

I realized that js variable was the basis for my “Single Source of Truth” and my API calls needed to dump into the initial on-load data variable at a bare minimum, a la the “Parallel, Centralized” approach.

But I also had moved all API-service calls together, and had thereby moved onto the “Service-Oriented” model.

That left me either making my own store-on-a-variable home-brew method, or to jump on the VueX train and learn about client-side data-stores.


Coding well includes knowing when you need to stop and look ahead.

What’s in a Store?

Eder Negrete Recently answered this question in a very succinct, but accurate way:

The data of the current view

The state of the current view

The “is” concept. (is loading?, is ready?, is current?, etc)

Shared data

We’ll be rolling forth on the “shared data” for the most part.

Code Dive-In:

This is our baseline from the previous version. Each component is talking to the API, albeit through a helper class.

// Direct-from-API Patternlet wordpressService = {  getFromAPI: function( path, resolve, reject ){    Vue.http.get( path ).then(response => {
let responseData = {posts: response.data, totalPages: 1};
resolve( responseData );
}).catch(error => reject(error));
},
getPageBySlug: function(page_slug) {
let path = "/wp-json/wp/v2/page/?slug="+page_slug;
return new Promise((resolve, reject) => {
this.getFromAPI( path, resolve, reject );
})
}
};// Component: => Service => API
import WordpressService from '../services/wordpress';
export default {
props: ['post_slug'],
created: function(){
this.getPage();
},
methods: {
getPageBySlug: function() {
const wpPromisedResult = WordpressService.getPageBySlug( this.post_slug );
wpPromisedResult.then(result => {
if( result.posts.length > 0){
this.page = result.posts[0];
}
}).catch(err => {
this.error = true;
});
}// getPage
}
}

When there is just the data-source and the presentation, it’s easy to understand: a component data becomes visible when the data shows up.

When there’s a Service & Store involved, it’s now a 3-body system (ask a Mechanical Engineer). Things are getting wonky.

How it actually happens

Here’s the high-level summary of the Component-Store-Service relationship:

Redefining the API/Service direction & having 2 functions with the same name looks redundant. I don’t recommend it, but I did it this way to show the parallels. A better approach may be to load a larger volume of data from the API, and use the store for specifics.

Most of my code for the Store is based off of Ogundipe Samuel’s very clear code from Getting Started with VueX.

// VueX Store Pattern:Vue.use(Vuex);
const store = new Vuex.Store({
state: {
posts: [],
posts_loading: false
},
mutations: { // Used by store.actions
STORE_POSTS: (state, { posts }) => {
state.posts.push(posts);
}
},
actions: { // Used by Components via dispatch()
FETCH_POSTS: function ({ commit }, get_object) {
const found_post = this.getters.getPost( get_object.type, get_object.slug );
if( typeof found_post !== 'undefined' ){ // In-Memory already
commit('SET_POSTS_LOADING', false);
}else { //no matches, hit the API
commit('SET_POSTS_LOADING', true);
WordpressService
.getPost( get_object.type, get_object.slug )
.then((response) => {
if( response.posts.length == 0 ) // OPTIONAL: Handle 404s
commit('STORE_POSTS', { posts: response.posts }); }, err => { // seems to be rare in normal circumstances.
console.log("FETCH_POST: err:", err, get_object );
commit('SET_POSTS_LOADING', false);
});
}
}
}, getters: { // Used by Component
getPostBySlug: (state) => (slug) => {
let foundit = state.posts.find(post => ( post.slug === slug && post.type == 'post')); if(typeof foundit == 'undefined') store.dispatch('FETCH_POSTS', { slug: slug } ); return state.posts.find(post => post.slug === slug);
}
}
});
export default store

The Component scripting becomes quite small, so long as the template has all the data it needs, which is assumed in this semi-contrived example:

// Component >> Store >> Service >> API<template>
<div class="page-wrapper">
<div class="article" v-if="(!posts_loading && this_post)">
<h1 class="article-title">{{ this_post.title.rendered }}</h1
</div>
</div>
</template>
<script>
import Vuex from 'vuex';
export default {
props: ['post_type', 'post_slug'],
created(){
this.$store.dispatch('FETCH_POST',
{ type: this.post_type, slug: this.post_slug} );
}, computed: {
...Vuex.mapState(['posts_loading']),
this_post: function(){ //Get the data from Store
let foundPostInStore = this.$store.getters.getPostBySlug(this.post_slug);
return foundPostInStore;
}
}
</script>

The Full Build-Out

Pulling & storing a page or post is the simplest scenario. Post Archives are much more complex: by year, by author, by term.. I simplified what I needed by using a mix of the WordPress database (everything is a post) with the basic info for each Archive header: Authors & Terms:

  • store.posts[]={id, post_type, post_meta, etc}
  • store.terms[] = { term_slug, taxonomy_name }
  • store.authors[] = {}

Taxonomies (a group of terms) just filter/reduce:

Vue.use(Vuex);
const store = new Vuex.Store({
getters: { // Used by ComponentgetTerms: (state) => ( requested_taxonomy_name, requested_term_slug ) => {
let found_term = [];
if( state.terms.length > 0 ){
found_term = state.terms.reduce(
function( accumulator, currentTerm ) {
if( currentTerm.taxonomy == requested_taxonomy_name && currentTerm.slug == requested_term_slug ){
accumulator.push( currentTerm );
}
return accumulator;
}, [] );
}
return found_term;
}
} //getters}); //store

I used the same approach for all archives: given a requested_author_slug , return all the posts in the Store that match. I’m also certain that lodash has a better method for my pure reduce. Note: UI pagination required a different approach.

The major upside?

As the user clicks around the site, the Store acquires more and more posts. Whenever the user clicks on something already in the store (like from a post-excerpt to a post-content) it’s instantly loaded. Instantly. Caching API requests comes close, but now we can:

  1. Pre-fetch! The UI pagination can be 4 posts per page, but the API pagination can be 20.
  2. Use the data across the whole session.

The major down-sides to this?

  1. Remember, the Store is just an in-memory variable. Don’t overload it. If you need more data saved, use IndexDB or LowDB. Your users have other things running on their device.
  2. Parallelism. I’m now writing the same function 2–3 times for each data type just to pass data along.
Parallelism is annoying

The WordPress API basically exposes each core database table. Nothing crazy. The basic routes follow the same pattern. Adding a Service to talk to the API is required, and adding a local Store is just another way to access the same thing.

How many times you want to refactor depends on how many times you want to re-implement data-access methods.

Choose your tradeoffs well. If the user will be blown away by 1 second load times over 0.1 second load times, then you’re good with infrastructural (CDN+Browser) caching for API requests.

You have 1 second. Choose Wisely.

Elephant in the room (may-or-may not be bull in china shop)

At some point, I’m unsure of why I’m recreating the WordPress API locally, other than for speed and it seems I’m not the only one:

Once you have a handful of stores or branches in your state tree, you end up practically duplicating your server-side business data and relationships on the client. — Mark Johnson

This is shown in its worst form with search. If the search component was hitting the API directly, WordPress itself is doing the search. Sure it’ll take an extra second and perhaps that’s fine but any store.getters.search function I would write is only searching the posts in-memory.

Wait, I’m building my own search algorithms?

This all makes sense. There’s this crazy guy who implemented his own WordPress API using GraphQL. I finally understand why. Generic REST routes can be like eating a plate of spaghetti one noodle at a time, or eating the AlphaBits cereal alphabetically.

Perhaps the real answer to the search problem is to just return the Post IDs?

Summary

There’s a debate/decision to be made about whether to use the local store as a caching mechanism or as an inter-component messaging system:

This nice how-to shows the inter-component messaging alternative to what I’m doing:

a good place to start looking for application state is your component props, and more specifically, props across multiple components that share the same data

A few common “gotchas”:

  • array.push is not a reactive way to update the state.
  • Hit the store/api during created() not mounted() You might also need to hit the store on beforeRouteUpdate(to, from, next).
  • Each component’s default state moved out of data(); now the default state is in computed:{}.
  • Chaining computed variables that are Promise-based still needs the Promise. Maybe using “data_loading” variables just needs to be a Promise!

Next Steps

I’ve starting using VueX as a cache-store, which isn’t what you hear most people talk about it for. Next up, I’ll try to actually extract the application state, perhaps on a store.current and perhaps a store.history variable.

Thanks for reading! You can compare the end-user difference the previous version:

..or checkout the code on it’s own branch:

M. Wallace

Written by

(Front|Back|Web) × (Developer|Ops) @WierStewart

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade