Arrow syntax is not a shorthand

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the javascript category.

Last Updated: 2024-11-21

I had a (Jest) mock of the browser's xhr functionality as follows:

const xhrMockClass = () => ({
  // This function (`send`) is the important bit for the purpose of this article
  send: () => {
    this.onreadystatechange()
    return jest.fn()
  },
  readyState: 4,
  status: 200,
  open: jest.fn(),
  onreadystatechange: jest.fn(),
  setRequestHeader: jest.fn()
})

window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)

The caller I wanted to test looked like this:

export function get(url, callback = () => {}) {
  const xhr = new XMLHttpRequest
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      callback(xhr)
    }
  }
  const async = true
  xhr.open("GET", url, async)
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
  return xhr.send()
}

When I ran the code in my test environment, I would get an error at xhr.send()

=> "Cannot read property 'onreadystatechange' of undefined"

When I debugged the mock by logging this, it turned out to be undefined

const xhrMockClass = () => ({
  send: () => {
    console.log(this)
  }
})

The fix was to change arrow invocation to invocation with the function keyword:

const xhrMockClass = () => ({
  ...
  send: function() {
    this.onreadystatechange()
    return jest.fn()
  },
})

Why on earth did this work? Let's take a more narrow example of the original (problem) code to explain what happened.

const test = () => ({
  state: 1,
  foo: () => {
    return this.state
  }
})

With this, test().foo() will return null instead of the 1 you might expect. This is because, this in foo is not bound to the object returned by the test function due to foo being being an arrow function!

This is so even if you test objects:

window.outer = {
  inner: {
    state: 1,
    foo: () => {
     return this.state
    }
   },
  state: "outerstate"
}
// outer.state.foo() still gives undefined

So don't use arrow functions with object methods... see here for discussion:

https://stackoverflow.com/questions/31095710/methods-in-es6-objects-using-arrow-functions

Rewrite with the method syntax instead to get it working:

const test = () => ({
  state: 1,
  foo() {
    return this.state
  }
})

Lesson

  1. T.J. Crowder on Stack Overflow put it best: "Arrow functions are not designed to be used in every situation merely as a shorter version of old-fashioned functions. They are not intended to replace function syntax using the function keyword. The most common use case for arrow functions is as short "lambdas" which do not redefine this, often used when passing a function as a callback to some function."

Therefore stick to method syntax or normal functions when defining functionality on objects.

  1. When using fat arrow functions, this is whatever the lexical environment is (i.e. the context it was defined). For example, in the following code, this is undefined, not xhrMockClass.
   const xhrMockClass = () => ({
    send: () => {
      this.onreadystatechange()
    }
   })

Think about it: - When we call xhrMockClass() it returns an object straight away - Before that object has been returned, we cannot possibly access it via this