Ionic Storage 3.x Vuejs list rendering issues

I’m trying to use Ionic Storage v3 in my Vuejs Ionic project but I am having issues rendering components after retrieving values from storage. I am pretty new to both VueJs and Ionic and definitely struggle with async functions and setup. With the setup below I am able to add items to storage and retrieve them but the array does not render in the dom. Any help would be appreciated.

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Test</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <div class="grid">
        <div class="grid-tile" v-for="project in projects" :key="project.id">
          <project-item :id="project.id" :title="project.title"></project-item>
        </div>
      </div>
      <ion-fab vertical="bottom" horizontal="end" slot="fixed">
        <ion-fab-button @click="projectModal(null,null)">
          <ion-icon :icon="addCircle"></ion-icon>
        </ion-fab-button>
      </ion-fab>
    </ion-content>
  </ion-page>
</template>

<script>
import {defineComponent} from 'vue';
import {IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonIcon, modalController, IonFabButton, IonFab} from '@ionic/vue';
import {addCircle} from 'ionicons/icons';
import ProjectItem from "@/components/ProjectItem";
import HawkProjectModal from "@/components/HawkProjectModal";
import {Drivers, Storage} from '@ionic/storage';
import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';

export default defineComponent({
  name: 'Project',
  components: {
    ProjectItem,
    IonPage,
    IonHeader,
    IonToolbar,
    IonTitle,
    IonContent,
    IonIcon,
    IonFabButton,
    IonFab
  },
  setup() {
    const projectDB = new Storage({
      name: 'projects',
      storeName: 'projects',
      driverOrder: [CordovaSQLiteDriver._driver, Drivers.IndexedDB, Drivers.LocalStorage]
    });
    
    return {
      projectDB,
      projects: [],
      addCircle,
    }
  },
  mounted() {
    this.initializeDatabase(this.projectDB);
    this.project = this.getDatabaseValues(this.projectDB)
  },
  methods: {
    async defineDriver(database) {
      await database.defineDriver(CordovaSQLiteDriver);
    },
    async createDatabase(database) {
      await database.create();
    },
    async initializeDatabase(database) {
      this.createDatabase(database)
          .then(this.defineDriver(database))
          .then(console.log(String(database._config.name) + ' database initialized'));
    },
    getDatabaseValues(database) {
      let values = [];
      database.forEach((value) => {
        values.push(value);
      });
      return values;
    },
    async setDatabaseEntry(database, key, value) {
      await database.set(key, value);
      return this.getDatabaseValues(database);
    },
    async projectModal(id, title) {
      const modal = await modalController
          .create({
            component: HawkProjectModal,
            cssClass: 'project-modal',
            componentProps: {
              id: id,
              title: title
            }
          });
      modal.onDidDismiss()
          .then((data) => {
            if (data.role === 'create' || data.role === 'edit') {
              const project = Object.assign({}, data.data);
              this.setDatabaseEntry(this.projectDB, project.id, {id: project.id, title: project.title}).then((newArray) => {
                this.projects = newArray;
              });
            }
          });
      return modal.present();
    }
  }
});
</script>

you are mixing old v2 vue with v3 vue.

I think you can get it working with this change

  setup() {

    const projects = ref([]); // new

    const projectDB = new Storage({
      name: 'projects',
      storeName: 'projects',
      driverOrder: [CordovaSQLiteDriver._driver, Drivers.IndexedDB, Drivers.LocalStorage]
    });
    
    return {
      projectDB,
      projects,
      addCircle,
    }
  },

Hi Aaron, thanks for looking over this for me. Correct, I’m pretty bad at mixing composition and options api syntax, but I don’t believe this is the issue. I tried your fix, and also fixed a typo in my code in the mounted option, but still don’t have anything populating in the dom. When I have Vue dev tools open I can see that projects is properly being populated within an array of values from IonicStorage, however the v-for doesn’t update and display those values. I’ve attached a screenshot from Vue dev tools. I’m thinking the issue is related to either lifecycle or more likely a misunderstanding of async functions. Again, any help would be greatly appreciated.

I think you need async/await within mounted and this.project should be this.projects (which I think you already fixed :slight_smile:). Without awaiting, initializeDatabase is getting called asynchronously so getDatabaseValues is getting called right away before this.projectDB is populated.

async mounted() {
    await this.initializeDatabase(this.projectDB);
    this.projects = this.getDatabaseValues(this.projectDB)
  },

Unfortunately that didn’t work either. Here’s the current setup I am testing:

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Test</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <div class="grid">
        <div class="grid-tile" v-for="project in projects" :key="project.id">
          <project-item :id="project.id" :title="project.title"></project-item>
        </div>
      </div>
      <ion-fab vertical="bottom" horizontal="end" slot="fixed">
        <ion-fab-button @click="projectModal(null,null)">
          <ion-icon :icon="addCircle"></ion-icon>
        </ion-fab-button>
      </ion-fab>
    </ion-content>
  </ion-page>
</template>

<script>
import {ref, defineComponent} from 'vue';
import {IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonIcon, modalController, IonFabButton, IonFab} from '@ionic/vue';
import {addCircle} from 'ionicons/icons';
import ProjectItem from "@/components/ProjectItem";
import HawkProjectModal from "@/components/HawkProjectModal";
import {Drivers, Storage} from '@ionic/storage';
import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';

export default defineComponent({
  name: 'Project',
  components: {
    ProjectItem,
    IonPage,
    IonHeader,
    IonToolbar,
    IonTitle,
    IonContent,
    IonIcon,
    IonFabButton,
    IonFab
  },
  setup() {
    const projects = ref([]);


    const projectDB = new Storage({
      name: 'projects',
      storeName: 'projects',
      driverOrder: [CordovaSQLiteDriver._driver, Drivers.IndexedDB, Drivers.LocalStorage]
    });

    return {
      projectDB,
      projects,
      addCircle,
    }
  },
  async mounted() {
    await this.initializeDatabase(this.projectDB);
    this.projects = this.getDatabaseValues(this.projectDB)
  },
  methods: {
    async defineDriver(database) {
      await database.defineDriver(CordovaSQLiteDriver);
    },
    async createDatabase(database) {
      await database.create();
    },
    async initializeDatabase(database) {
      this.createDatabase(database)
          .then(this.defineDriver(database))
          .then(console.log(String(database._config.name) + ' database initialized'));
    },
    getDatabaseValues(database) {
      let values = [];
      database.forEach((value) => {
        values.push(value);
      });
      return values;
    },
    async setDatabaseEntry(database, key, value) {
      await database.set(key, value);
      return this.getDatabaseValues(database);
    },
    async projectModal(id, title) {
      const modal = await modalController
          .create({
            component: HawkProjectModal,
            cssClass: 'project-modal',
            componentProps: {
              id: id,
              title: title
            }
          });
      modal.onDidDismiss()
          .then((data) => {
            if (data.role === 'create' || data.role === 'edit') {
              const project = Object.assign({}, data.data);
              this.setDatabaseEntry(this.projectDB, project.id, {id: project.id, title: project.title}).then((newArray) => {
                this.projects = newArray;
              });
            }
          });
      return modal.present();
    }
  }
});
</script>

How about an await on line 73?

async initializeDatabase(database) {
    await this.createDatabase(database)
        .then(this.defineDriver(database))
        .then(console.log(String(database._config.name) + ' database initialized'));
},

Or you need to call

this.projects = this.getDatabaseValues(this.projectDB)

once the createDatabase promise resolves in a then. I am also not sure you can have multiple thens. You should be able to do all your work within one.

I don’t believe the issue is related to the DB not being defined or created, in that scenario I would receive an error instructing me to initialize the DB. I’ve tested this loading the page without my initialize function. Below is from ionic-storage:

  private assertDb(): Database {
    if (!this._db) {
      throw new Error('Database not created. Must call create() first');
    }

    return this._db!;
  }

I think the problem is more related to the promise of forEach() resolving after the dom has already populated, and Vue not updating the dom after that promise resolves. Again from ionic-storage:

  /**
   * Iterate through each key,value pair.
   * @param iteratorCallback a callback of the form (value, key, iterationNumber)
   * @returns Returns a promise that resolves when the iteration has finished.
   */
  forEach(
    iteratorCallback: (value: any, key: string, iterationNumber: Number) => any
  ): Promise<void> {
    const db = this.assertDb();
    return db.iterate(iteratorCallback);
  }

Are you able to provide a sample project with the issue happening?

1 Like

@twestrick absolutely, see below:

@aaronksaunders any chance you have plans to release a tutorial using Ionic + Vuejs + Ionic-storage v3 on your youtube channel? :sweat_smile:

Here is some working code. The root problem is that you were reassigning the value of tasks instead of just pushing to the array you already instantiated with ref([]). I think because an array is not a primitive type, reassigning tasks.value causes Vue to lose the reactivity.

setup() {
    const tasks = ref([]);
    const tasksDB = new Storage({
        name: "tasks",
        storeName: "tasks",
        driverOrder: [
            CordovaSQLiteDriver._driver,
            Drivers.IndexedDB,
            Drivers.LocalStorage,
        ],
    });

    //Ionic-storage functions
    const setDatabaseEntry = async (database, key, value) => {
        await database.set(key, value);
        tasks.value.push(value);
    };

    const getDatabaseValues = (database) => {
        database.forEach((value) => {
            tasks.value.push(value);
        });
    };

    const initializeDatabase = async (database) => {
        await database.defineDriver(CordovaSQLiteDriver);
        await database.create();
        console.log(
            String(database._config.name) + " database initialized"
        );
        getDatabaseValues(database);
    };

    //Popup modal for list item creation
    const taskModal = async () => {
        const modal = await modalController.create({
            component: TaskModal,
            cssClass: "task-modal",
        });
        modal.onDidDismiss().then((data) => {
            if (data.role === "create") {
                let task = Object.assign({}, data.data);
                setDatabaseEntry(tasksDB, task.id, task);
            }
        });
        return modal.present();
    };

    initializeDatabase(tasksDB);

    return {
        addCircle,
        caretForwardCircle,
        tasks,
        tasksDB,
        taskModal,
    };
},

You’re right, that does seem to work better. My assumption was that I needed to replace the whole array in order for the dom to properly update. I think it was this section in Vue’s docs that set me down that path.

When working with non-mutating methods, you can replace the old array with the new one:

example1.items = example1.items.filter(item => item.message.match(/Foo/))

You might think this will cause Vue to throw away the existing DOM and re-render the entire list - luckily, that is not the case. Vue implements some smart heuristics to maximize DOM element reuse, so replacing an array with another array containing overlapping objects is a very efficient operation.

Thanks so much for taking the time to look into this for me!