Async data to child page with Ionic5

I’m trying to pass data between a parent and a child page in ionic using Ionic Storage to get the data in an async way.

What is happening is that when I get to the page, the data didn’t return from the storage yet and I have an undefined error: ERROR TypeError: Cannot read property ‘name’ of undefined.

What I am using:

  • A parent page that I click in an item in the grid and it forwards me to the child page, using router.navigate
  goToMediaDetails(data) {
    this.router.navigate([`slate-list/${data.id}`]);
  }
  • The child route is listed in the app-routing.module.ts receiving the id
  {
    path: "slate-list/:mediaId",
    loadChildren: () =>
      import("./pages/slate-list/slate-list.module").then(
        m => m.SlateListPageModule
      )
  }
  • I grab the mediaId in the child’s constructor page and use a service to get the data from ionic storage.
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";

//Services
import { MediaService } from "../../services/media.service";

@Component({
  selector: "app-slate-list",
  templateUrl: "./slate-list.page.html",
  styleUrls: ["./slate-list.page.scss"]
})
export class SlateListPage implements OnInit {
  public media: any;
  private mediaId: number;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private mediaSvc: MediaService
  ) {
    //if the id is provided in the url, get the media by id using the service
    if (route.snapshot.params.mediaId) {
      this.mediaId = route.snapshot.params.mediaId;
      this.mediaSvc.getMediaById(this.mediaId).then(result => {
        this.media = result;
      });
    }
  }
  • Here is the service code returning a promise
  //GET Media By ID
  getMediaById(mediaId) {
    let mediaToReturn = new Media();
    return new Promise(resolve => {
      this.storage.get("media").then(result => {
        if (result != null && result.length != 0) {
          mediaToReturn = result.find(x => x.id == mediaId);
        }

        resolve(mediaToReturn);
      });
    });
  }
  • Here is the simple html giving the problem
<ion-content>
  <ion-grid class="ion-no-padding">
    <ion-row>
      Slates for <strong>{{media.name}} </strong> / Episode: {{media.episode}}
    </ion-row>
  </ion-grid>
</ion-content>

Yes, the data is returned using the service, I console.log it right after the .then and the data is there, so I’m assuming that it’s just a classic async situation.

I saw I can introduce a loading component, make it show up for 1 second or a bit more and then the data will be there but is that the better/official way to do it?

I’m at the beginning of my journey with ionic/async so forgive me if I made some silly mistake

since your media object is being assigned asynchronously, it is undefined when angular creates the template and is unable to read the properties inside it. You could either add *ngIf="media" to the <ion-row> or put a ? before the .'s anywhere you are accessing media’s properties. Ex: {{media?.name}} The question mark will check that media is available before attempting to access it’s property

1 Like

Thank you @christhompson05! The ? operator solved the problem.

1 Like

There’s a third option, which I generally prefer to the other two in @christhompson05’s post for largely aesthetic and philosophical reasons: always assign a sensible default value to any controller property that is referenced from a template at definition time if possible, or in the constructor if it can’t be done then for technical reasons.

// never use `any` if you can avoid it
interface Media {
  id: string;
  name: string;
  episode: string;
}

class SlateListPage {
  media = {} as Media;
}

Incidentally, you’ve fallen into the explicit promise construction antipattern trap. There’s virtually never any need to be typing new Promise in an Ionic app (or, frankly, new anything), and certainly not here:

library$: Promise<Media[]>;

constructor(private storage: Storage) {
  this.library$ = storage.ready().then(() => storage.get("media") || []);
}

getMediaById(mediaId: string): Promise<Media> {
  return this.library$.then(library => library.find(x => x.id = mediaId));
}
2 Likes

Thank for sharing your knowledge, Robert.

As I said in my original post, I’m a newbie in this Ionic/Async world, so I’m reading and testing a lot. I’ve been assigned to this task as I got to this position with no mobile development background.

This project is using not interface, but a class to define the Media data structure. I’ll read more about the differences using it as an interface or class. I’ve trying to set is as a new Media, but I was getting problems. Doing your way (media = {} as Media) solved the problem.

Regarding your antipattern trap advice, this is a bit confusing for me at this initial point. Could you clarify the use of this variable library$? I could see that it gets the whole media key in the storage and then I search from there. Would not it be using more data back and forth on the network than I really need when I do the find command to search only for that specific id?

Thank you again!

I’ve discussed why I make this choice several times here. This thread is probably the most comprehensive.

The main point of it is to ensure that storage is ready before you try to read from it. Failure to do this is the iceberg upon which many ships on these forums have sunk, with the final distress call from their doomed captains sounding “everything worked fine on the browser, but when we deployed it to a particular FrobozzCo phone, storage stopped working”. But it also makes your code more efficient and aligns it to a rule of mine that you may not like very much at this point, now that I reread your OP:

I really discourage doing this. See here for a case study on why. The aforementioned rule:

Never use Storage to communicate within runs of an app. Use it only to talk to the future and listen to the past.

So in my previous post I was assuming that you were using Storage as basically an offline cache.

Storage is a “NoSQL” system, meaning it’s just keys and values. I was trying not to be too disruptive to your existing code, which seemed to me to be storing all media in an array. So I preserved that, with the difference that your getMediaById hits storage every single call, whereas mine defers all storage interaction to the service constructor, so there is only one storage read each app run. After the read from storage has finished, library$.then will trigger immediately, because the Promise has already resolved, so performance will be improved.

I’m not sure where “on the network” comes in, because storage is just grabbing stuff from on-device memory. One option would be to store each media object separately in storage, but unless you have hundreds or maybe even thousands of media (which would probably make other UI aspects of your app more pressing concerns than storage performance) I highly doubt that would be a worthwhile endeavour. Certainly not a road I would travel unless profiling indicated necessity.