Skip to content

Instantly share code, notes, and snippets.

@redconfetti
Last active August 30, 2021 01:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save redconfetti/11883d7842c212790155fa96ea4cb727 to your computer and use it in GitHub Desktop.
Save redconfetti/11883d7842c212790155fa96ea4cb727 to your computer and use it in GitHub Desktop.
Asynchronous HTTP Requests in JavaScript with Fetch, Async, and Await

Pure JavaScript

JavaScript makes AJAX requests asynchronously, so you can't expect the code to wait for a return value. You have to use a callback function to process the data when it is returned.

In the past you would use a library to make HTTP requests, like jQuery, the AngularJS $http service, or the Axios library (used in older ReactJS applications).

Now JavaScript supports a new API called Fetch.

These libraries, and Fetch, return an object known as a Promise object. The Promise wraps the actual request function in a handler that enables you configure callback functions that are called upon success or failure of the request.

const data = { username: 'example' };

fetch('https://example.com/profile', {
  method: 'POST', // or 'PUT'
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data),
})
.then(response => response.json())
.then(data => {
  console.log('Success:', data);
})
.catch((error) => {
  console.error('Error:', error);
});

So if you have multiple calls to the back-end that need to be called in a specific order, you need to chain together multiple callbacks.

For example, if I need to:

  1. Create Lead
  2. Create QuickTest owned by Lead
  3. Request StoryTest (QuickTest converted to StoryMap format)
const createLead = function() {
  return fetch('https://www.enneagramtest.net/lead', {method: 'POST'});
};

const storeLeadData = function(leadData) {
  store.lead = leadData;
};

const createQuickTest = function() {
  return fetch('https://www.enneagramtest.net/quick_test', {
    method: 'POST',
    data: body: JSON.stringify({
      lead_id: lead_id
    })
  });
};

const storeQuickTest = function(quickTestData) {
  store.quick_test = quickTestData;
};

const loadStoryTest = function() {
  return fetch('https://www.enneagramtest.net/story_test/' + store.quick_test.id);
};

createLead()
  .then(storeLeadData)
  .then(createQuickTest);
  .then(loadStoryTest);

A newer version of Ecmascript (ES6, ES2015) supports two new features that make this easier to code for, known as 'async' and 'await'.

By putting the async keyword in front of a function definition, it causes the function to return a Promise object.

let hello = async function() { return "Hello" };

hello().then((value) => console.log(value))

The 'await' keyword can be used within an async function to block the execution of the function call until it has a value to return.

async function loadStoryTest() {
  let lead = await createLead();
  let quickTest = await createQuickTest(lead.id);
  let storyTest = await getStoryTest(quickTest.id);
};

But this isn't the ultimate solution to my problem, because I'm using the Vuex data store, which has it's own kind of structure around how you define your functions that fetch data and store that data in the global data store.

Vuex

Within ReactJS applications, the Redux library is used to maintain a global data store that maintains the data state throughout the application. Vuex is the equivalent of this library intended for use with VueJS applications.

A data store makes it possible for your website components to subscribe to data changes in the store, and re-render their data when the state changes.

It also enables you to view the incremental changes step-by-step that have occurred to the state, which helps with debugging. Using the Vuex DevTools plugin for Chrome, you can view the mutations to the state that are being "committed", and the exact state as it is with each committed mutation.

Test

State

The state is a single object that stores the entire global state tree used throughout your application.

Mutations

To update the state using Vuex, you must define mutation functions that perform the mutations to the state(s).

An approach required with mutation functions is that they must take the current state and replace it with a new state object that is different. The entire object must be replaced so that the change is detected, and so that components that are subscribing to changes can be notified and re-render using the new state data.

If you were to directly reference a property of a state object and modifying it, that would bypass the callbacks that occur after a state change.

ES6 introduced a new feature known as destructuring that is helpful in creating new objects that are based on the previous objects.

let person = {firstName: 'Toby', lastName: 'Smith'};
let personWithScore = {...person, score: 95};
console.log(personWithScore); // {firstName: 'Toby', lastName: 'Smith', score: 95};

The ...person returns the properties of the person variable, which are then included in the definition of the personWithScore object. personWithScore is an entirely different object, not a mutated/modified version of the person object.

Here is an example of mutation functions defined to manage a single part of the state labelled 'userSession'.

export const CREATE_USER_SESSION_FAILED = 'CREATE_USER_SESSION_FAILED'
export const CREATE_USER_SESSION_LOADED = 'CREATE_USER_SESSION_LOADED'
export const CREATE_USER_SESSION_START = 'CREATE_USER_SESSION_START'

const store = new Vuex.Store({
  state: {
    userSession: undefined,
  },
  mutations: {
    [CREATE_USER_SESSION_FAILED] (state, errorMessage) {
      state.userSession = { ...state.userSession, loading: true, error: true, errorMessage: errorMessage }
    },
    [CREATE_USER_SESSION_LOADED] (state, user) {
      state.userSession = { ...state.userSession, user: user, loading: false }
    },
    [CREATE_USER_SESSION_START] (state) {
      state.userSessio = { ...state.userSession, loading: true, error: false, errorMessage: null }
    },
  },
  actions: {}
})

Commit

You do not make calls to mutation functions directly, you use the commit function provided by the store to trigger a mutation.

You can include an object that acts as the 'payload' for the mutation function to use. {name: 'Toby Smith'} is our payload in this example.

import { UPDATE_CUSTOMER_NAME } from '../store/mutation-types';

export const setCustomerName = (name) => {
  store.commit(UPDATE_CUSTOMER_NAME, {name: 'Toby Smith'});
}

It's a common practice to define constants for the strings that are used to represent the different mutation types you can commit.

As you can see above we're importing the UPDATE_CUSTOMER_NAME constant which is simply a string with "UPDATE_CUSTOMER_NAME" as its value.

Mutations Must Be Synchronous

Your mutation functions cannot make asynchronous calls, such as requests to to external HTTP APIs for data. If you did this it would invalidate the ability to rely on the Vuex DevTools to debug the application. It would show a mutation being logged, but the actual change to the state might happen at a different time, or in a different order of sequence with other mutations.

Actions

Vuex supports functions known as Actions, which are meant to be the functions that can obtain data asynchronously and then use store.commit() to update the state via the appropriate mutation function after it's received.

const store = new Vuex.Store({
  state: {
    user: undefined
  },
  mutations: {
    setUser (state, user) {
      state.user = {...state.user, user: user}
    }
  },
  actions: {
    setUser (context) {
      context.commit('setUser')
    }
  }
})

To trigger calls to the defined actions, you use the dispatch() function.

store.dispatch('setUser', user)

Composing Actions

Because your Action functions can be asynchronous, you'll want to compose your Actions by using Promises to chain together different actions, or use async/await to define the order of calls.

This is the ultimate solution to making ordered calls to your back-end HTTP API, while keeping your code simpler and easier to maintain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment