Hotwire Discussion

Accessing `this` from `fetch`

Hello everyone,

I’m trying to write some DRY Stimulus controllers but constantly run into a problem with fetch — I cannot access this.

Given this example:

export default class extends Controller {
    static targets = [ 'country', 'regions' ]

    connect(e) {
        this.handle()
    }

    handle(e) {
        const authenticityToken = document.querySelector('meta[name="csrf-token"]').content
        const country_id = this.countryTarget.options[this.countryTarget.selectedIndex].value

        const url = '/countries/' + country_id

        fetch(url, {
            credentials: 'include',
            method: 'GET',
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json',
              'X-CSRF-Token': authenticityToken
            }
        })
            .then(response => response.json())
            .then(function(data) {
                if (!data.regions) {
                    return
                }

                // Populate this.regionsTarget
            })
    }
}

What would I need to do to access this? I’m not having luck using await fetch as it throws a Babel compiler error in webpack.

(post deleted by author)

This might help:

converting to an arrow function should fix your issue

.then((data) => {
                if (!data.regions) {
                    return
                }

                // Populate this.regionsTarget
            })

What I’m learning is I need to really brush up on my JavaScript skills.

I ended up using both suggestions here and I broke the populating of the regions down into a separate function. I’m not sure if this is the cleanest approach — if this were pure Ruby I would prefer to make the populateRegions function private.

Here is my final result (note that the JSON response now has country and regions is a child of that object):

export default class extends Controller {
    static targets = ['country', 'regions']

    connect(e) {
        this.handle()
    }

    handle(e) {
        const authenticityToken = document.querySelector('meta[name="csrf-token"]').content
        const country_id = this.countryTarget.options[this.countryTarget.selectedIndex].value

        const url = '/countries/' + country_id + '.json'

        fetch(url, {
                credentials: 'include',
                method: 'GET',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': authenticityToken
                }
            })
            .then(response => response.json())
            .then(response => this.populateRegions(response.country))
    }

    populateRegions(country) {
        const selected_value = this.regionsTarget.options[this.regionsTarget.selectedIndex].value

        Array.from(this.regionsTarget.options).forEach(function(option) {
            option.remove()
        })

        country.regions.forEach(function(region) {
            const option = document.createElement('option')
            option.text = region.name
            option.value = region.id

            if (option.value == selected_value) {
                option.selected = true
            }

            this.regionsTarget.add(option)
        }.bind(this))
    }
}

You can accomplish something similar as:

export default class extends Controller {
  static targets = ['country', 'regions']

  connect(e) {
    this.handle()
  }

  handle(e) {
    const authenticityToken = document.querySelector('meta[name="csrf-token"]').content
    const country_id = this.countryTarget.options[this.countryTarget.selectedIndex].value

    const url = '/countries/' + country_id + '.json'

    fetch(url, {
      credentials: 'include',
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-CSRF-Token': authenticityToken
      }
    })
      .then(response => response.json())
      .then(response => populateRegions(response.country, this.regionsTarget))
  }
}

function populateRegions(country, target) {
  const selected_value = target.options[target.selectedIndex].value

  Array.from(target.options).forEach((option) => option.remove())

  country.regions.forEach((region) => {
    const option = document.createElement('option')
    option.text = region.name
    option.value = region.id
    option.selected = (region.id == selected_value)
    target.add(option)
  })
}

That actually makes a lot of sense. Yep, a JavaScript brush-up is in order. Thanks!