ArcGIS JS API 4.x not supported with VueJS 3.0?

7901
14
Jump to solution
10-28-2020 06:52 AM
Jay_Gregory
Occasional Contributor III

Esri's JS API worked beautifully with VueJS 2.x - however trying to incorporate into a VueJS 3.0 application is proving to be challenging, and I'm wondering if the incompatibilities are irreconcilable without significant changes to the API. 

Although I can't know for sure, I think the issue revolves around that Vue3.0 wraps all reactive objects in a JavaScript Proxy object, which allows it to intercept / trap getters and setters. 

From Vue's own documentation:

The use of Proxy does introduce a new caveat to be aware with: the proxied object is not equal to the original object in terms of identity comparison (===). For example:  The original and the wrapped version will behave the same in most cases, but be aware that they will fail operations that rely on strong identity comparisons.

Creating a simple map with a feature layer, for example, if the feature layer is a reactive object in the VueJS 3.0 ecosystem, gives some strange errors and requires a workaround.  The following code is using the new Composition API in Vue (that is why I reference everything with the value property but it is the correct programming pattern here), and throws an error at the last line - the layer is never added to the map. To those familiar with the Composition API, using a reactive object (instead of ref like in the code) generates the same issues. 

let featurelayer = ref(null)
featurelayer.value = new FeatureLayer(url);
map.add(featurelayer.value);

 Uncaught (in promise) TypeError: 'get' on proxy: property '__accessor__' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#<a>' but got '[object Object]')

However, if I change the last line to 

map.add(featurelayer.value.load());

The layer is successfully added.  An acceptable but annoying workaround as an application scales. 

There are other issues however for which I have been unable to find a workaround.  

For example:

mapView.whenLayerView(featurelayer.value).then((layerView) => {
   console.log(layerView);
});‍‍‍‍‍‍‍‍‍

throws the following error:

 Uncaught (in promise) e {name: "view:no-layerview-for-layer", details: {…}, message: "No layerview has been found for the layer". 

Using featurelayer.value.load() or featurelayer.value.createLayerView() does not work.  

So ultimately I'm curious if anyone knows what is happening with these seeming incompatibilities, and more specifically, if the Esri JS API team would ever consider making the changes necessary to play well with a (increasing in popularity) framework such as VueJS 3.x.

Tags (4)
0 Kudos
14 Replies
by Anonymous User
Not applicable

No problem.  This concerns me a bit too as we use Vue for all of our apps as well.  They are currently all written in Vue 2.x, but future apps may be built with 3.x.  We use vuex heavily for state management in larger apps and if we cannot store esri objects there due the conflicts with Accessor, we may be in trouble.

Hopefully refactoring won't be too bad and will likely involve shifting things from Vue watchers to accessor watchers/watchUtils in the created() method.  I know in the past in terms of watching things like graphics array updates, we have always used esri watchers for that since they are Collections and not actually native Arrays.

The best solution is what you mentioned though, which is that hopefully esri can address this somehow.  I feel this will present some major challenges as the conflict seems to be tied to reactivity/getters/setters built-in into Accessor, which is at the core of almost everything in the JS API.  

 

Jay_Gregory
Occasional Contributor III

I have the same concerns with Vuex too....and agree that their modifying Accessor to support Proxy would be a tall order.  

0 Kudos
Jay_Gregory
Occasional Contributor III

@Anonymous User  -

I've been taking a look at the code and was curious if you think your code could be adjusted to work with esri-loader?  I'm having a little trouble getting it to work, since the setup function needs to include onMounted (due to the asych nature of esri-loader's loadModules), but the return portion of setup (outside the onMounted callback) would only be able to return reactive objects (which cannot be Esri objects as we've determined).  

The reason I ask is because vue-cli with @arcgis/core build time leaves something to be desired.  

In other words, would https://github.com/crackernutter/vuejs3-arcgisapi-test/blob/main/src/App.vue be able to use esri-loader, or does it have to use the ES6 modules of Esri's API?

0 Kudos
Jay_Gregory
Occasional Contributor III

Just wanted to loop back with you on this and thank you again.  I forgot you can add non tracked/non-reactive objects to a component instance, so that seemed to do the trick to get me up and running.

Unfortunately, we use Vuex a lot too, particularly to store the MapView to pass to different components & widgets across the app.  I just tried implementing this (vuex storing the MapVuew) for a feature table, and while the table renderers, the synchronized behavior with the actual map unfortunately does not work (ie, editing, center on click, highlight, etc).  After digging into the Vue reactivity documentation, I found markRaw and toRaw, which can be used on Esri’s object to prevent them from being wrapped in a Proxy or strip away the proxy, respectively.  I think it’s exactly what you’d need to use Esri’s JS objects in Vuex.

Thanks!

0 Kudos
by Anonymous User
Not applicable

@Jay_Gregory 

This was a bit tricky, as it requires using an Async Setup Method and Suspense.  And I totally get it, adding @arcgis/core to the app makes the dev build much slower and production builds much larger.  I created an "esri-loader" branch in my fork of the repo.  So to use the esri-loader, I made the following tweaks:

Created a new Component called Mapview.vue:

 

import { defineComponent, nextTick } from "vue";
import { loadModules } from 'esri-loader'

export default defineComponent({
  
  async setup() {
    /**
     * do we really want vue to make these reactive? Are they really going to change?
     * this will add extra overhead. If we need to handle reactive changes on esri stuff,
     * we can take advantage by using "esri/core/watchUtils" or by using the Accessor.watch()
     */
    
    try {
      const [Map, MapView, FeatureLayer] = await loadModules(["esri/Map", "esri/views/MapView", "esri/layers/FeatureLayer"])
    
      // work with esri modules here
      // instead declare them as static data properties
      const map = new Map({
        basemap: "topo-vector",
      });

      const view = new MapView({
        container: "esri-map", // just pass this here to set required property
        map,
        zoom: 5,
        center: [-95, 39],
        popup: {
          autoOpenEnabled: false,
        },
      });

      const layer = new FeatureLayer({
        portalItem: {
          id: "9e2f2b544c954fda9cd13b7f3e6eebce",
        },
        outFields: ["*"],
        title: "Recent Earthquakes",
      });

      map.add(layer)

      return {
        view,
        map,
        layer
      }

    
    } catch (error) { 
      // handle any script or module loading errors
      console.error(error)
    }
    
  },

  created(){
    // verify we have access to view, layer
    console.log('view: ', this.view)
    console.log('map: ', this.map)
    
    /**
     * the map actually won't render from our setup() because the
     * "esri-map" div does not exist yet, so we must re-set it here 
     */
    nextTick(()=> {
      // I was having issues with race conditions here, had to set a small timeout
      setTimeout(()=> {
        this.view.container = this.$refs.esriMap
        console.log('view container: ', this.view.container)
      }, 150)
    })

    // here is where we can actually test things out
    this.view.whenLayerView(this.layer).then((layerView) => {
      console.log("layerview generated");
      console.log(layerView);
    });
  }
});

 

And then using Suspense in the App.vue template:

 

<template>
  <div id="container">
    <Suspense>
      <template #default>
        <!-- main async content goes here -->
        <map-view />
      </template>

      <template #fallback>
        <!-- fallback content goes here while component is loading -->
        <h3 class="loader">Loading...</h3>
      </template>

    </Suspense>
  </div>
</template>

 

Also, if you like using the esri-loader, I built a cool VS Code Plugin that auto completes the loadModules() function (also supports TypeScript):

plugin-async-js.gif

0 Kudos